Thread Sync #1. 概念篇 - 如何化被動為主動?

別以為我轉行了... 這篇不是勵志文章,教你用主動積極的態度面對人生.... 而是討論執行緒同步機制及如何用來解決惱人的流程問題。會寫這篇的念頭來自黑暗程式魔人辦的猜數字程式設計大賽,在處理的過程中想到的解法...,不過這篇要講的不是猜數字,而是不相干的東西: Thread Sync (執行緒的同步機制)。

 

一般程式寫久了,會很習慣一路到底的思考方式,程式也完全照這樣的思路被設計出來。不過寫 GAME 這類的程式就不是這麼一回事了。就先舉十五年前我用 C 寫的俄羅斯方塊的遊戲當例子 (大驚! 十... 十五年?),腦袋裡想的流程八九不離十,一定是像這樣:

"隨機從上面掉一個方塊下來,時間到了就往下掉,USER有按方向鍵就左右移動或是旋轉,直到卡到底下或是其它方塊為止..."

很正確的想法,很可惜你的程式完全不能這樣寫,為什麼? 當你沒有使用多執行緒或是其它的技巧時,你的主程式流程一定得被限定在固定時間 refresh 畫面的無窮迴圈... 上面的邏輯怎麼辦? 會被迫拆成好幾塊,然後被主程式定期呼叫... 程式寫起來大概會像這樣:

TETRIS
   1:  public void ProcessBrick()
   2:  {
   3:      switch (status)
   4:      {
   5:          case 1:
   6:              //
   7:              //  按右鍵, 往右移一格
   8:              //
   9:              break;
  10:          case 2:
  11:              //
  12:              //  按左鍵, 往左移一格
  13:              //
  14:              break;
  15:          case 3:
  16:              //
  17:              //  按上鍵, 順時針旋轉 90 度
  18:              //
  19:              break;
  20:          case 4:
  21:              //
  22:              //  按下鍵, 往下移一格
  23:              //
  24:              break;
  25:          case 5:
  26:              //
  27:              //  .......
  28:              //
  29:              break;
  30:      }
  31:  }

 

原本好好的邏輯被切成好幾塊,然後再藉著狀態等資訊,每次的 LOOP 各挑這次要執行的那一小段,然後拼湊出原本的邏輯...。別哀怨,誰叫你寫的 CODE 不是老大? 老大是控制畫面的主程式,你既然是當小的就乖乖躲在旁邊被呼叫... 委屈一點是應該的...。

 

真是黑心啊... 誰叫老闆永遠是對的。這次黑心... 不,黑暗魔人出的題目正好又讓我聯想到一樣的狀況。黑暗魔人出的題目,是先實作了 GameHost 類別 (就是老大啦),及 Player 抽象型別 (小角色就是他),再藉著多型 (Polymorphism) 的方式,由 GameHost 不斷的呼叫 Player 提供的 GuessNum( ),來讓 Player 問問題,同時把上一次問題的答案傳給 Player ...

 

程式也不難寫,大家都玩過 1A1B 的遊戲吧? 腦袋裡一定是這樣想的:

 

"一開始先隨便猜幾個,把結果記下來....。"

"再來刪去法,比較可能的幾個數字再猜一猜...。"

"快猜到了,幾種組合列出來想一想怎麼辦好? 好! 就猜這個...。"

...

...

...

"BINGO! 猜中了!!!"

 

很高興的想好流程後,真的要開始寫就傻住了... 這堆流程跟邏輯,要我拆成一直會被重複呼叫的 "單一個" method, 每一回合會被呼叫一次...。意思是一連串複雜的處理過程,要依序切成完全一樣的片段 (就是指重複呼叫同一段程式碼) ? 更慘的是這次問的問題,下一次呼叫才拿的到答案!? MY GOD... 要想出怎麼猜到對方的數字已經夠頭痛了,還得來處理這些 "行政" 問題?

 

My God, 頭越想越痛,這樣下去來我大概只能寫出比 DummyPlayer (註: 參賽程式附的 Player 範例,隨機產生問題,無腦的一直問,直到猜中為止...) 高明一點點的程式而已了。曾 MSN 跟黑暗魔人討論過,看看能不能讓 GameHost 化主動為被動,改由 GameHost 提供 callback 讓 Player 呼叫的可能性? 不過後來想想不對,那有主持人在旁邊看等著被來賓訪問的道理? 何況如果以後是兩個 Player 對戰怎麼辦? 誰要來當 "小的" ?? 問題還是一樣沒解決...

 

過去大家對於 THREAD 的印象都是 "多工",需要用 thread 解決的問題大多是在改善執行效能上面,因為同時用兩個 thread 可以做兩件事,效率會比較好。其實 thread 也很適合解決這類的問題,因為執行緒讓我們有機會,不需切斷 GameHost / Player 的 "思路",讓兩邊都能用很直覺的思考方式寫程式。

 

大概畫一下時序圖對照一下,先看看原本的作法:

投影片2

 

 

 

 

再看一下改用兩個執行緒的作法:

投影片3

 

 

不管之中的技術障礙怎麼克服,至少這樣改起來,兩邊都能各自用更合理簡單的方式思考自己的問題,也更接近實際的情況 (莊家跟玩家不會共用一個腦子吧...)。也唯有把問題簡化之後,我們才有辦法想出更複雜的方式來解決問題,科技不就是這樣進步的嘛?

 

這次的例子裡,執行緒是用來簡化問題的,而不是拿來增進效率的。兩個人腦袋各自想著問題,總要溝通吧? thread 之間溝通的機制就很單純了,共用變數加上同步機制,來確定對方是否準備好我要的東西,或是對方是否已經準備好要接招了?

 

這次搬出來的是過去說明 thread pool 提到的 AutoResetEvent,現在又重現江湖了。方法很簡單,要拿資料的那一方,就去 Wait( ) 等資料準備好,另一方把資料放在共用變數之後就呼叫 Set( ),叫醒另一個還在 Wait( ) 的執行緒,可以起床拿東西閃人了...。

 

接下來當然就各忙各的,直到雙方又有交換資料的需求,同樣的方式就再來一次。只是隨著資料交換的方向不同 (比如問問題是把題目由 player --> host,而取得答案則是 host --> player),上述的動作雙方角色要互換才能順利進行。

 

Orz,本來想一篇打完的,不過打到快睡著,加上內容還剩不少... 就分兩集吧! 程式碼及實作各位就耐心點,下一篇會端出實際操作執行緒的範例。






安德魯部落格 GPTs

試試用 GPTs 幫你讀文章!
直接用白話文詢問,"安德魯的部落格 GPTs" 會幫你找到相關文章,也會用我文章的知識來回答你的問題。

Facebook Pages

Edit Post (Pull Request)

Post Directory