併發級別:阻塞、無障礙、無鎖、無等待
一般認為併發可以分為阻塞與非阻塞,對於非阻塞可以進一步細分為無障礙、無鎖、無等待,下面就對這幾個併發級別,作一些簡單的介紹。
併發級別
1、阻塞
阻塞是指一個執行緒進入臨界區後,其它執行緒就必須在臨界區外等待,待進去的執行緒執行完任務離開臨界區後,其它執行緒才能再進去。
2、無障礙(obstruction-free)
無障礙是一種最弱的非阻塞排程。兩個執行緒如果是無障礙的執行,那麼他們不會因為臨界區的問題導致一方被掛起。換言之,大家都進入臨界區了。那麼如果一起修改共享資料,把資料改壞了可怎麼辦呢?對於無障礙的執行緒來說,一旦檢測到這種情況,它就會立即對自己所做的修改進行回滾,確保資料安全。從這個策略中也可以看到,當臨界區中存在嚴重的衝突時,所有的執行緒可能都會不斷地回滾自己的操作,而沒有一個執行緒可以走出臨界區。這種情況會影響系統的正常執行。
一種可行的無障礙實現可以依賴一個“一致性標記”來實現。執行緒在操作之前,先讀取並儲存這個標記,在操作完成後,再次讀取,檢查這個標記是否被更改過,如果兩者是一致的,則說明資源訪問沒有衝突。如果不一致,則說明資源可能在操作過程中與其他寫執行緒衝突,需要重試操作。而任何對資源有修改操作的執行緒,在修改資料前,都需要更新這個一致性標記,表示資料不再安全。
跟非阻塞排程比較,阻塞排程可以認為是一種悲觀的策略,它會認為多個執行緒一起修改資料會使資料損壞,所以阻塞排程每次只能允許一個執行緒去修改資料。而非阻塞排程相對來說比較樂觀,它認為如果多個執行緒一起修改也未必會把造成資料損壞,所以它允許多個執行緒同時進入臨界區,但無障礙是一種寬進嚴出的策略,進的時候不作限制,所有的執行緒都能進入臨界區做其想做的事情,包括讀與寫,但是出來的時候就不那麼寬鬆了,如果一個執行緒在臨界區中的操作遇到了資料競爭,跟其它執行緒產生了衝突,它就會回滾這條資料,然後重試自己的操作。比如讀取x與y的值,這個操作是分步進行的先讀x,再讀y,當讀完x,發現別的執行緒修改了x,再讀y就已經沒有意義了,因為可能會讀到一個錯誤的資料,所以該執行緒會重試,再去讀取一次,直到自己讀到的x、y沒有問題為止,所以無障礙是一種會不斷重試的排程策略,但它會保證沒有資料競爭時,執行緒必然能在有限的步驟內執行完任務。
在無障礙的排程方式當中,所有的執行緒都相當於在拿取一個系統當前的快照,它們會一直重試,直到拿到的快照有效為止。
3、無鎖(lock-free)
是無障礙的
保證有一個執行緒可以勝出
前面說的無障礙是指所有的執行緒都能進入臨界區,但如果發生了競爭,無障礙並不保證臨界區的執行緒能夠順利的出來,因為如果執行緒發現自己的資料每次去讀取或者去操作,總是跟其它執行緒產生衝突,它就會不停地重試,如果在臨界區當中有10個執行緒,執行緒1修改了部分資料,結果它被執行緒2干擾了,執行緒2又被執行緒3干擾,依此類推,最後執行緒1它又可能去幹擾執行緒10,如果它們之間是彼此干擾的,最終會導致所有的執行緒都卡死在裡面,系統的效能會受到比較嚴重的影響,因此,無鎖必須在無障礙的基礎上加一個約束,保證在競爭當中有一個執行緒是必然能夠勝出的,這樣就能保證在臨界區的執行緒當中至少有一個是能順利走出去的,而不至於全部在裡面陣亡掉,如果至少有一個執行緒能夠出去,那麼就有第二個執行緒能夠出去,假設裡面有一百個執行緒,第一個執行緒競爭勝利,走出了臨界區,剩下99個再競爭又必然能勝利一個,因為每次競爭它必然保證能有一個勝利,使得系統至少是能夠順暢的執行下去的,這就是無鎖,下面這段程式碼在java當中是比較典型的使用無鎖的程式碼:
while(!hyes.compareAndSet(localHyes,localHyes+1)){
localHyes = hyes.get();
}
在高併發多執行緒中,CAS(Compare And Swap,比較交換)技術就是一種無鎖實現.在它的實現中,使用了一個無限迴圈,當要修改的內容和期望內容一致時,才去做修改.因此,CAS對死鎖是免疫的.在java.util.concurrent.atomic包下(在jdk的rt.jar中)的各種原子類實現,都使用了CAS技術.例如在AtomicInteger中的getAndSet(int newValue)方法.
另外,使用無鎖方式,省去了執行緒之間競爭臨界區資源鎖而產生的效能損耗,也沒有執行緒之間頻繁排程帶來的開銷.
4、無等待(wait-free)
無鎖的
要求所有的執行緒都必須在有限步內完成
無飢餓的
前面說了無鎖是能保證至少有一個執行緒能夠在有限步當中完成它的操作,所有的執行緒在不停地競爭直到有一個勝出為止。無等待相比於無鎖更進一步,它首先要求是無鎖的,保證所有執行緒能進並且至少有一個執行緒能出來,同時無等待它在提高要求,它要求所有進入臨界區的執行緒都能夠在有限步當中完成其操作,這個要求很高,因為任何執行緒都能夠無障礙進入臨界區,並且任何執行緒都能夠在有限步當中完成操作後離開臨界區,這就會使得整個系統的執行變得非常順暢,無等待可以說是並行最高級別了,它基本上能使整個系統發揮到最好佳效率。
無等待必須然也是無飢餓的,因為所有的執行緒都能在有限步當中完成,因此必然不會有執行緒永久地呆在臨界區內出不去,所以它一定是無飢餓的。
無等待的一個典型案例是,有讀寫兩個執行緒,如果說只有讀執行緒沒有寫執行緒,那麼所有的讀執行緒之間必然是無等待的,因為讀不會修改資料,如果有一個寫執行緒在裡面,由於會修改資料 ,寫執行緒必然會導致讀執行緒不是無等待。因此可以提出一種演算法去作一點改進,比如說有一種演算法它會這樣做,因為寫可能會影響到讀,所以每次寫之前先把資料拷貝一份副本,執行緒修改的是這個副本而非原始資料,修改資料的過程可能需要一點時間,因為修改的是副本資料而不是原始資料,所以這個修改的過程也不影響執行緒讀,因此在這個過程當中所有的讀執行緒一樣是無等待的,它們都能夠在有限的步驟當中完成自己的操作,而所有的寫執行緒相對來講,因為每個寫執行緒它都是寫自己的副本,因此它們的寫也是無等待的,所以它們都不需要去跟彼此作同步,最後需要同步的只是將寫完之後的資料覆蓋原始資料,而這個覆蓋原始資料的動作是非常快的,因為我們並不需要作大量的寫操作,只不過是一個指標或引用作一個替換而已,不管哪個寫執行緒勝出,總是能夠保證替換上去的資料是一致的,並不像其它的演算法一樣,可能會把資料寫壞,因為大家都寫的是副本,最後是一個指標指向誰的問題,這樣資料必然是安全的,這種方式它就是無等待的一個典型的實現。
無等待表示任何執行緒都可以在有限步驟內結束,而不必關心其他執行緒進度如何.
進一步分類可以分為有界無等待Wait-Free Bounded (WFB)和集居數無關無等待Wait-Free Population Oblivious.
有界無等待:按照英文願意,是指方法的執行過程都可以在有界限的步驟內完成,但是這個過程可能是與執行緒數量相關的.
集居數無關無等待(也可以叫做執行緒數無關無等待):在英文文獻中,是這麼說的–一個無等待的方法,如果其效能和活動執行緒數目無關,那麼被稱為集居數無關無等待的。
Wait-free bounded(有界無等待):
如果所有的L個執行緒消耗C(N,L)或者更少的時間完成操作:OpsF() < C(N,L)
Wait-free population oblivious(集居數無關無等待,在併發變成實戰中翻譯成了執行緒數無關無等待,也準確):
如果所有的L個執行緒在有限操作內完成F,並且和L無關:OpsF() < C(N).其中,設F為一個函式方法,設L為同時呼叫F的併發執行緒數目,設N為一個與L無關的變數,設OpsF()代表一個指定執行緒完成F需要進行的操作步驟,設C(N,L)為一個依賴N和L的函式.
一種典型的無等待結構就是Read-Copy-Update(RCU).它的基本思想是,對資料的讀可以不加控制,因此,所有的讀執行緒都是無等待的.但是在寫資料的時候,需要先取得原始資料的副本,接著修改副本資料,修改完成後,然後在合適的時機回寫資料.
無等待的實現,在所有併發等級中是最麻煩的,而且技巧性的東西會比較多,相對來說,無鎖的使用會更加廣泛一些。