1. 程式人生 > >併發資料結構-1.1 併發的資料結構的設計

併發資料結構-1.1 併發的資料結構的設計

原文連結譯文連結,譯者:董明鑫,校對:周可人

隨著多個處理器共享同一記憶體的機器在商業上的廣泛使用,併發程式設計的藝術也產生了巨大的變化。當前的趨勢向著低功耗晶片級多執行緒(CMT)發展,所以這樣的機器一定會更加廣泛的被使用。

共享記憶體多處理器是指併發的執行多個執行緒的系統,這些執行緒在共享的記憶體中通過資料結構通訊和同步。這些資料結構的效率對於效能是很關鍵的,而目前熟練掌握為多處理器機器設計高效資料結構這一技術的人並不多。對大多數人來說,設計併發的資料結構比設計單執行緒的難多了,因為併發執行的執行緒可能會多種方式地交錯執行他們的指令,每一種方式會帶來不同的,甚至不符合預期的輸出。這就要求設計者改變他們對運算的認識,理解新的設計方法,採用新的程式設計工具集。此外,設計可擴充套件的併發資料結構,使得當機器執行越來越多的併發執行緒時依舊錶現良好也是新的挑戰。本文主要介紹設計併發資料結構的相關挑戰,和一些重要的資料結構相關內容的總結。我們的總結絕不是全面的;相反,我們特意選取了一些能說明設計的關鍵問題的流行的資料結構,希望我們提供了足夠的背景和知識,讓有興趣的讀者接觸那些我們沒有提到的內容。

1.1 併發的資料結構的設計

共享記憶體多處理器的一些特性使得併發資料結構的設計和校驗比相對應的單執行緒結構難度顯著增加。

acquire(Lock);

oldval = X;                                                                                      oldval = X;

X = oldval + 1;                                                                                X = oldval + 1;

return oldval;                                                                                 release(Lock);

return oldval;

圖1.1:順序的和基於鎖機制的fetch-and-inc操作程式碼片段

難點的根源在於併發:因為執行緒是在不同的處理器上併發的執行,而且受作業系統的排程決策、缺頁、中斷等等影響,我們必須按照全部非同步的想法來思考,以保證不同的執行緒能夠隨意交錯的執行。這顯著提升了正確設計併發資料結構的複雜度。

為多處理器系統設計併發的資料結構在效能和可擴充套件性上也有大量的挑戰。在現代的機器上,處理器和記憶體的佈局、資料在記憶體中的佈局、多處理器體系結構中各個元素間的通訊負載全都對效能有影響。此外,正確性和效能兩者聯絡非常的緊密:演算法的改進在提高效能的同時經常使其更加難以設計和檢驗正確的資料結構實現。

下面的例子闡述了影響資料結構設計的各個多處理器特徵。假設我們想要實現一個共享的計數器資料結構,用於支援fetch-and-inc操作,即計數器加一然後返回增加前的值。一個普通的順序fetch-and-inc實現的程式碼就如圖1.1中左邊部分所展示的那樣。

如果我們允許多個執行緒併發地呼叫fetch-and-inc操作,上述實現執行起來並不正確。來看看原因,注意大多數編譯器會把這份原始碼轉換成機器指令:把 X 的值裝進一個暫存器,然後把暫存器中的值加一,然後再把這個暫存器的值存回 X 。假如計數器初始化為 0 ,兩個不同的處理器併發的執行兩個fetch-and-inc操作。然後就有可能兩個操作都從 X 中讀出 0 ,然後都把 1 存回 X 並且返回 0 。這顯然是不正確的:兩個操作中有一個應該返回 1 。

如上所述,兩個fetch-and-inc操作不正確的交錯結果導致了不正確的行為。一個自然並且普通的方法來阻止這樣的交錯就是用互斥鎖(也被叫做 mutex 或者 lock)。鎖是一個在任意時間點,都是不被(其他執行緒)獲取,只被一個執行緒所獲取的構造。如果一個執行緒 t1 希望獲取已經被另一個執行緒 t2 所獲取到的鎖,那麼 t1 必須等到 t2 釋放這個鎖。

如圖 1.1 右半部分所示,我們能通過鎖機制得到一個正確的順序實現。我們通過阻止所有的交錯來預防壞的交錯。這樣很容易得到一個正確的共享計數器,然而這種簡單是有代價的:鎖機制引發了許多關於效能和軟體工程上的問題。


董明鑫

即將畢業學生一枚,業餘程式設計師 -> [email protected]