別以為我轉行了… 這篇不是勵志文章,教你用主動積極的態度面對人生…. 而是討論執行緒同步機制及如何用來解決惱人的流程問題。會寫這篇的念頭來自黑暗程式魔人辦的猜數字程式設計大賽,在處理的過程中想到的解法…,不過這篇要講的不是猜數字,而是不相干的東西: Thread Sync (執行緒的同步機制)。
一般程式寫久了,會很習慣一路到底的思考方式,程式也完全照這樣的思路被設計出來。不過寫 GAME 這類的程式就不是這麼一回事了。就先舉十五年前我用 C 寫的俄羅斯方塊的遊戲當例子 (大驚! 十… 十五年?),腦袋裡想的流程八九不離十,一定是像這樣:
「隨機從上面掉一個方塊下來,時間到了就往下掉,USER有按方向鍵就左右移動或是旋轉,直到卡到底下或是其它方塊為止…」
很正確的想法,很可惜你的程式完全不能這樣寫,為什麼? 當你沒有使用多執行緒或是其它的技巧時,你的主程式流程一定得被限定在固定時間 refresh 畫面的無窮迴圈… 上面的邏輯怎麼辦? 會被迫拆成好幾塊,然後被主程式定期呼叫… 程式寫起來大概會像這樣:
TETRIS
public void ProcessBrick()
{
switch (status)
{
case 1:
//
// 按右鍵, 往右移一格
//
break;
case 2:
//
// 按左鍵, 往左移一格
//
break;
case 3:
//
// 按上鍵, 順時針旋轉 90 度
//
break;
case 4:
//
// 按下鍵, 往下移一格
//
break;
case 5:
//
// .......
//
break;
}
}
原本好好的邏輯被切成好幾塊,然後再藉著狀態等資訊,每次的 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 的 “思路”,讓兩邊都能用很直覺的思考方式寫程式。
大概畫一下時序圖對照一下,先看看原本的作法:
再看一下改用兩個執行緒的作法:
不管之中的技術障礙怎麼克服,至少這樣改起來,兩邊都能各自用更合理簡單的方式思考自己的問題,也更接近實際的情況 (莊家跟玩家不會共用一個腦子吧…)。也唯有把問題簡化之後,我們才有辦法想出更複雜的方式來解決問題,科技不就是這樣進步的嘛?
這次的例子裡,執行緒是用來簡化問題的,而不是拿來增進效率的。兩個人腦袋各自想著問題,總要溝通吧? thread 之間溝通的機制就很單純了,共用變數加上同步機制,來確定對方是否準備好我要的東西,或是對方是否已經準備好要接招了?
這次搬出來的是過去說明 thread pool 提到的 AutoResetEvent,現在又重現江湖了。方法很簡單,要拿資料的那一方,就去 Wait( ) 等資料準備好,另一方把資料放在共用變數之後就呼叫 Set( ),叫醒另一個還在 Wait( ) 的執行緒,可以起床拿東西閃人了…。
接下來當然就各忙各的,直到雙方又有交換資料的需求,同樣的方式就再來一次。只是隨著資料交換的方向不同 (比如問問題是把題目由 player –> host,而取得答案則是 host –> player),上述的動作雙方角色要互換才能順利進行。
Orz,本來想一篇打完的,不過打到快睡著,加上內容還剩不少… 就分兩集吧! 程式碼及實作各位就耐心點,下一篇會端出實際操作執行緒的範例。