多核編程中的線程隨機競爭模式的機率分析 前一篇
多核編程中的線程分組競爭模式中談到了讓線程分組競爭以解決多核CPU遇到的鎖競爭導致的饑餓問題。並不是任意的共用資料都能夠設計成進行分組競爭的模式,比如最常用的需要用於尋找的資料結構,當資料結構分成多個子資料結構後,每次尋找時,不能指定尋找某個特定的子資料結構,而必須進行二級尋找,先在整個資料結構內找到對應的子資料結構(不加鎖),然後再在子資料結構中尋找(加鎖)。如果同時多個線程進行尋找,有可能尋找的資料分布在不同的子資料結構裡,也可能分布在同一子資料結構中。當尋找分布在同一子資料結構時,這時就有可能發生鎖競爭現象,從而引起CPU饑餓的發生。在這種分布式資料結構的隨機鎖競爭中,需要知道的是在一個k個核的CPU上,需要的線程數m和劃分的子資料結構個數n為多少時,才能保證至少有k個線程在同時啟動並執行機率不低於給定的機率P。首先m必須大於等於k,否則無法保證至少有k個任務在運行。子資料結構個數N也必須大於K,否則出現競爭的工作群組數將少於k個,從而無法保證至少有k個任務在運行,當然n越大,任務出現競爭的機率就越小,同時啟動並執行線程數量就越多,不妨設n大於等於m。在實際情況中,n並不是越大越好,當 n過大時,由於鎖的數量和n相等,會導致鎖佔用過多的系統資源。下面就來計算一下至少有k個線程在同時啟動並執行機率,考慮一種最壞情況的假設:假設有兩個線程在訪問同一個子資料結構 ,那麼它們一定會發生鎖競爭。在這種最壞假設下,要保證至少有k個線程在同時運行 ,實際上相當於m個線程至少訪問了k個不同的子資料結構。假設訪問每個子資料結構的線程數為Xi ( 0 <= Xi <= m, i∈{1,2,…n}),這樣可以得到以下整數方程:X1+X2+…+Xn = m (方程1)要保證至少有k組線程在競爭,實際上相當於X1,X2…Xn中必須至少有k個大於0,這樣至少有k個線程在啟動並執行機率相當於上述方程滿足,X2…Xn中必須至少有k個大於0的解的個數和所有可能解的個數的比值。下面是對這個機率公式的一些實際計算結果:
當k=2
(2
核CPU
), m=2
(2
個線程), P=(n-1) / (n+1) 當n=4時,P=0.6; 當n=8時,P=7/9 =0.7778; 當n=16時, P=15/17=0.882
當k=2
(2
核CPU
), m=4
(4
個線程), P=(n-1) (n
+3)/ ((n+1)(n+2)) + 9 (n-1)/((n+3)(n+2)(n+1)) 當n=4時,P=0.83; 當n=8時,P=0.919; 當n=16時, P=0.954
當k=4
(4
核CPU
), m=4
(4
個線程), P=(n-1) (n-2)(n-3)/ ((n+1)(n+2)(n+3)) 當n=4時,P=0.0286; 當n=8時,P=0.212; 當n=16時, P=0.47; 當n=32時,P=0.687
當k=4
(4
核CPU
), m=6
(6
個線程), P = [ 1+12(n+15)/((n+4)(n+5)) ]
×[(n-1)(n-2)(n-3)]/ [(n+1)(n+2)(n+3)] 當n=8時,P=0.587; 當n=16時, P=0.886; 當n=32時,P=0.978從上面計算可以看出,當CPU核心數固定時,線程數m越多,則機率愈大 ,子資料結構個數n越大,機率愈大。一般來說線程數最好比核心數大一倍,這樣得出的機率會大一些。以上計算的是在最壞情況下的機率,實際情況中,由於兩個線程在競爭同一個子資料結構時並不一定會發生競爭現象,因為可能發生線程A在進行鎖操作時,線程B正在執行不需要加鎖部分的代碼,因此實際的機率會大於上面計算出的最壞情況下的機率。
分布式資料結構隨機鎖競爭和無鎖編程的效能比較在使用了隨機鎖競爭的分布式資料結構中,並行化的加速比期望值等於前面所計算出的機率×CPU核心數,因此只要將機率保持大於一定的值,那麼加速比是可以得到保證的,並且只要加大線程個數和子資料結構個數,那麼加速比的期望值就會增加。另外分布式資料結構中相比於單線程的資料結構其操作要複雜一些,增加了一些計算開銷,另外加上鎖的計算開銷,因此加速比要打一個較大的折扣。但是分布式資料結構的好處在於它的加速比係數不會隨CPU核心數的增加而降低,程式的效能是隨著核心數的增加而線形增加的(前提是在資料 結構中的元素個數足夠多的情況下)。在無鎖編程中,由於使用了原子操作,原子操作是序列化的,雖然原子操作占的比重很小,但是這種序列化反映到加速比計算上需要按照阿姆爾達定律來計算,因此其效能同樣不容樂觀,會隨著CPU核心數的增加而降低。以一個無鎖的FIFO隊列為例,在進隊操作時需要使用一條CAS原子操作,由於隊列操作本身就很簡單,因此昂貴的CAS操作所佔的比例也不容小覷,在這種隊列操作中,CAS所佔的比例估計要達到20%左右(具體的資料需要通過測試才能確定),按照阿姆爾達定律,在一個8核的 CPU上的加速比係數將為3.33, 在一個64核CPU上,其加速比將小於5,當然這是只考慮隊列操作沒有考慮程式中其他並行操作的極端情況,但是不管怎麼說,採用無鎖編程的話,加速比係數會隨CPU核心數的增加而降低。另外無鎖編程相比於單線程編程,其代碼也變複雜了,也增加了額外的計算開銷,加速比也需要另外打一個折扣。如果將分布式資料結構和單核時的多線程編程相比,則分布式資料結構中,僅僅增加了定位到子資料結構的開銷,如果是尋找類型的資料結構,子表的尋找時間縮小了,實際上增加的開銷小於定位子資料結構的開銷。因此分布式資料結構增加的開銷所佔的比例是非常小的,其效能近似(略低)於單核時的多線程編程。在CPU核心數較少時,無鎖編程的效能可能會優於分布式資料結構,並且優於單核多線程編程的效能,但是當CPU核心數增加到一定程度時,分布式資料結構的效能優勢就體現出來了。採用分布式資料結構可以複用部分單線程時的資料結構代碼,採用加鎖機制容易被程式員理解,並且實現的功能不受限制。而無鎖編程則難度非常高,遠非普通程式員所能掌握,並且實現的功能受到限制,比如實現一個無鎖的隊列,如果想要給隊列加一個計數來掌握隊列中有多少元素,採用無鎖編程實現估計就很難行得通了,而這在有鎖編程中只是一個簡單得不能再簡單的東西。因此對程式員來說,分布式資料結構是多核時代必需掌握的技術,而無鎖編程也許可以用在某些無法使用分布式資料結構的特定場合。需要說明的是前面對機率的計算隱含了一個前提,就是每個線程在訪問各個子資料結構時的機率是相同的,這要求各個子資料結構必須是負載平衡的,否則如果訪問各個子資料結構的機率不相同的話,計算出的結果會小於前面的計算結果,考慮一種最極端的情況,所有的資料都在一個子資料結構裡,那麼所有的線程都將競爭同一個子資料結構,那麼問題倒退回多核編程中的鎖競爭難題一文中描述一樣的情況,這是一種可能比阿姆爾達定律更糟糕的情況。100%的負載平衡是做不到的,所幸可以通過一定的手段來使資料盡量變得均衡,使得資料能夠相對較均勻地分布在各個子資料結構中,這樣就不會對最終的機率產生較大影響。 參考資料:[1]《並行編程模式》Timothy Mattson等著 敖富江譯[2]《多核程式設計技術》Shameem Akhter等著 李寶峰等譯[3] “An Optimistic Approach to Lock-Free FIFO Queues” Edya Ladan-Mozes and Nir Shavit [4]《並行程式設計》 Barry Wilkinson, Michael Allen著,陸鑫達等譯[5]“Fast and lock-free concurrent priority queues for multi-thread systems” H. Sundell, Philippas Tsigas, Journal of Parallel and Distributed Computing. 2005[6]“ NOBLE: A Non-Blocking Inter-Process Communication Library.” H. SUNDELL, P. TSIGAS.
Proceedings ofthe 6th Workshop on Languages, Compilers and Runtime Systems for Scalable omputers (LCR’02), Lecture Notes in Computer Science, Springer Verlag, 2002.