“生命遊戲”的多執行緒演算法思考

myan發表於2020-04-04
Intel正在ISN網站上舉辦一個多執行緒程式設計大賽,值得關注。Intel過去幾年舉辦過好幾次執行緒技術大賽,包括與topcoder合作的一些競賽,質量都不錯。題目難度適中,而且具有啟發性,對多核程式設計感興趣的C/C++程式設計師應該關注一下。其實參與這樣的活動,置身於競賽氣氛當中,無論是否獲獎,都可以在短時間內大幅度地提高對多執行緒程式設計的理解。這次比賽比較有特色,為期長達幾個月之久,而且每個月都有一輪競賽,每月評選一輪優勝獎,獎品也很誘人,是一顆4核的酷睿2CPU ;-)

本月(2008年1月)的題目是一個經典問題“生命遊戲”。這是英國數學家John Conway發明的一個有趣的遊戲。不過這個遊戲之所以名聲大噪,還得歸功於著名科普作家馬丁•伽德納。他在1970年10月號的《科學美國人》雜誌“數學遊戲”專欄介紹了生命遊戲,不但讓大眾迷上這個遊戲,也令多專業數學家產生了研究的興趣,甚至產生了一個新的數學研究領域cellular automata(細胞自動機?),聽說這個領域還對模擬類遊戲產生了影響,不知是否確有其事。

大致來說,生命遊戲是這樣玩的。在一個由正方形小格子組成的二維網格(就像國際象棋棋盤那樣)裡,生活著一群細胞。每一個細胞佔據一個小格子。它的四鄰左右(最多)有8個格子,如果那些格子裡也生活著細胞,那麼這些細胞就成為“鄰居”。

細胞對生存環境要求苛刻,太孤獨的話會死。但是食物有限,所以太擁擠也會死。細胞還需要繁衍生息,如果鄰居數量合適,就可以在空格子裡分裂出新的細胞。數學家們研究的話題是,怎樣制定規則才能讓細胞群的繁衍發展呈現某種特定的模式,比如說,能夠穩定持續地發展下去,或者逐漸消亡,或者恆定不變,或者,最有趣的是,在幾個狀態之內反覆迴圈。要想“生生不息”,這些條件即不能太嚴苛,也不能太寬鬆。總之,Conway對於生命遊戲制定的規則如下:

1.    如果一個細胞只有0或1個鄰居,它將因為孤獨而死;
2.    如果一個細胞有4到8個鄰居,它將因為擁擠而死;
3.    如果一個細胞恰有2或者3個鄰居,它將繼續生存下去;
4.    如果一個空格子恰有3個鄰居,將“生”出一個新細胞;
5.    其他的空格子繼續維持原狀。

這個問題出現在程式設計師面前的時候,大多數是要求開發一個程式來對這個遊戲進行模擬。我以前學習資料結構和演算法的時候曾經接觸過,但是沒有深入思考。其實這個問題很有趣,演算法上可以有些變化,但主要是資料結構的設計,可以有幾種不同的考慮。比如可以從網格角度出發,設計一個(可能是稀疏的)矩陣,用0或1表示細胞的生死,每過一代就對矩陣進行一次全域性掃描,決定細胞的生死。也可以從細胞出發,把每個細胞的二維座標位置記下,然後一個細胞一個細胞地考察,沒考察一個細胞,就把它可能影響到的其他細胞或者空格納入視線,等到全部細胞考察完畢,也就可以一次性決定下一代的格局。後面這種演算法減少了需要考慮的情況數量,當網格很大而細胞比較稀疏時就節省了時間。

不過這次Intel的競賽並不想讓參賽者在演算法上動腦筋,而是已經把序列程式實現了,要求參賽者在其基礎上改成並行多執行緒程式。可以從競賽站點上分別下載LinuxWindows版的序列程式實現。其中的演算法基本上是上述第二種,即從細胞出發的演算法。程式當中自制了一個超級簡單的連結串列資料結構,然後用四個連結串列newlive, newdie, maylive, maydie,分別表示新生的,剛死的,可能出生的和可能死掉的細胞。然後用TraverseList函式遍歷連結串列,並對連結串列中的每一個元素施加相應的操作。最後把結果一口氣寫在一個5002x5002的網格中。實際上這個網格的有效尺度是5000x5000,多出來的那兩行和兩列,是為了方便邊界條件的處理。整個演算法還是很直白的,建議有興趣的人直接下載程式來閱讀理解。

我大致思考了一下,實際上這個題目還是屬於一個資料並行的演算法,關鍵在與把連結串列的訪問函式並行化,包括ClearList,TraverseList,CopyList,如果能夠充分並行化,則可以利用多核CPU的多個硬體執行緒加速程式的執行。如果是為了這個目標,簡單的單連結串列就似乎不是最好的資料結構,而類似STL中std::vector那樣的動態陣列就比較適合,因為可以很有效的分段。我的大致想法如下:

用std::vector取代List作為基本資料結構,設定一個合適的grain size,也就是說一個執行緒處理的細胞數量。一般來說,這個數量的大小大致以需要消耗10,000條機器指令為好,我估計在這個例子中,100是一個合理的數值。然後根據現有細胞數量,確定執行緒數N。主執行緒建立N個執行緒後呼叫join阻塞自己,等待那N個執行緒分別在List的不同片段上執行相同的操作。當所有的執行緒執行完畢之後,就可以得到所需的結果。這裡還應該用執行緒池來避免頻繁地建立執行緒。至於演算法,直接用程式中提供的即可。這個題目主要還是考察執行緒操作。

這裡的難點還是在那些底層的執行緒API。我不太熟悉pthread,對Windows執行緒還是比較熟悉的。儘管如此,讓我去寫一個小的執行緒池,然後再把每一個片段任務分配給執行緒來執行,再處理同步什麼的,還是有點麻煩。直接使用OpenMP或者Intel Threading Building Blocks就會方便很多。我傾向於使用後者,不過還需要學習。

這是我的是一些思考,有興趣的朋友不妨試試做一下這個題目。

Intel執行緒挑戰賽的網址是:
 http://softwarecontests-zho.intel.com/threadingchallenge/

在網頁下方可以看到這個題目的詳細資訊,並且可以下載序列範例程式碼。








 

相關文章