1. 程式人生 > >002_重拾多執行緒之多執行緒併發級別

002_重拾多執行緒之多執行緒併發級別

併發級別:

多執行緒之間的併發必須受到控制。根據控制併發的策略,我們可以把併發的級別進行分類,大致上可以分為阻塞、無飢餓、無障礙、無鎖、無等待幾種。

1.阻塞(blocking)

       一個執行緒是阻塞的,那麼在其他執行緒釋放資源之前,當前執行緒無法繼續執行。當我們使用synchronized關鍵字,或者重入鎖時,我們得到的就是阻塞的執行緒。

      無論是synchronized 或者重入鎖,都會試圖在執行後續程式碼前,得到臨界區的鎖,如果得不到,執行緒就會被掛起等待,直到佔有了所需資源為止。

2.無飢餓(starvation-Free)

如果執行緒之間是有優先順序的,那麼執行緒排程的時候總是會傾向於滿足高優先順序的執行緒。也就是說,對於同一個資源的分配,是不公平的!對於非公平的鎖來說,系統允許高優先順序的執行緒插隊。這樣有可能導致低優先順序執行緒產生飢餓。但如果鎖是公平的,滿足先來後到,那麼飢餓就不會產生,不管新來的執行緒優先順序多高,要想獲得資源,就必須乖乖排隊。那麼所有的執行緒都有機會執行。

3.無障礙(Obstruction-Free)

       無障礙一種最弱的非阻塞排程。兩個執行緒如果是無障礙的執行,那麼他們不會因為臨界區的問題導致一方被掛起。換言之,大家都可以大搖大擺地進入臨界區了。那麼如果大家一起修改共享資料,把資料改壞了可怎麼辦呢?對於無障礙的執行緒來說,一旦檢測到這種情況,它就會立即對自己所做的修改進行回滾,確保資料安全。但如果沒有資料競爭發生,那麼執行緒就可以順利完成自己的工作,走出臨界區。

       如果說阻塞的控制方式是悲觀策略。也就是說,系統認為兩個執行緒之間很有可能發生不幸的衝突,因此,以保護共享資料為第一優先順序。相對來說,非阻塞的排程就是一種樂觀的策略。它認為多個執行緒之間很有可能不會發生衝突,或者說這種概率不大。因此大家都應該無障礙的執行,但是一旦檢測到衝突,就應該進行回滾。

       從這個策略可以看出,無障礙的多執行緒程式並不一定能順暢的執行。因為當臨界區中存在嚴重衝突時候,所有的執行緒可能都會不斷地回滾自己的操作,而沒有一個執行緒可以走出臨界區。這種情況會影響系統的正常執行。所以,我們可能會非常希望在這堆執行緒中,至少可以有一個執行緒能夠在有限的時間內完成自己的操作,而退出臨界區。至少這樣可以保證系統不會在臨界區中進行無線的等待。

       一種可行的無障礙實現可以依賴一個‘一致性標記’來實現。執行緒在操作之前,先讀取並儲存這個標記,在操作完成後,再次讀取,檢查這個標記是否被更改過,如果兩者是一致的,則說明資源訪問沒有衝突。如果不一致,則說明資源在操作過程中與其他寫執行緒衝突,需要重試操作。而任何對資源有修改操作的執行緒,在修改資料前,都需要更新這個一致性標記,表示資料不再安全。

4.無鎖(Lock-Free)

       無鎖的並行都是無障礙的。在無鎖的情況下,所有的執行緒都能常識對臨界區進行訪問,但不同的是,無鎖的併發保證必然有一個執行緒能夠在有限步內完成操作離開臨界區。

        在無鎖的呼叫中,一個典型的特點是可能會包含一個無窮迴圈。在這個迴圈中,執行緒會不斷常識修改共享變數。如果沒有衝突,修改成功,那麼程式退出,否則繼續嘗試修改。但無論如何,無鎖的並行總能保證有一個執行緒是可以勝出的,不至於全軍覆沒。至於臨界區中競爭失敗的執行緒,它們則必須不斷重試,直到自己獲勝。如果運氣不好,總是嘗試不成功,則會出現累死飢餓的現象,執行緒會停止不前。

       

while(!atomicVar.compareAndSet(localVar, localVar+1)){
    localVar = atomicVar.get();
}

5.無等待(wait-Free)

       無等待是在無鎖基礎上更進一步擴充套件。它要求所有的執行緒都必須在有限步驟內完成,這樣就不會引起飢餓問題。如果限制這個步驟上限,還可以進一步分解為有界無等待和執行緒數無關的無等待幾種,它們之間的區別只是對迴圈次數的限制不同。

       一種典型的無等待結構就是RCU(Read-Copy-Update)。基本思想是,對資料的讀可以不加控制。因此,所有的讀執行緒都是無等待的,它們既不會被鎖定等待也不會引起任何衝突。但在寫資料的時候,先取得原始資料的副本,接著只修改副本資料(這就是為啥讀可以不加控制),修改完後,在合適的時機回寫資料。