1. 程式人生 > >並行程式設計中的lock free技術

並行程式設計中的lock free技術

lock free (中文一般叫“無鎖”,一般指的都是基於CAS指令的無鎖技術) 是利用處理器的一些特殊的原子指令來避免傳統並行設計中對鎖(lock)的使用。

眾所周知,鎖在解決並行過程中資源訪問問題的同時可能會引入諸多新的問題,比如死鎖(dead lock),另外鎖的申請/釋放對效能也有不小的影響,當然最大的問題還在於使用鎖的程式碼模組通常難以進行組合。lock free的目標就是要消除鎖對程式設計帶來的不利影響。

C++大牛Andrei Alexandrescu (就是把template玩得爐火純青的那個gg,《MODERN C++ DESIGN》的作者)的文章《Lock-Free Data Structures

》是lock free方面的代表作,有興趣的最好看一下,就當開闊思路吧。(btw, Andrei這哥們兒好像就是愛玩點bt的東西,呵呵)。

不過lock free本身也是目前各種並行解決方案中比較受爭議的一種: 一來這項技術有點過於詭異,掌握起來頗有難度,不過另一方面,因為它是完全基於最基本的程式設計技術,所以並不依賴任何語言/平臺,理論上應用面可以很廣。

在並行程式設計方面,函式式的那些東西(比如ErlangHaskell之類的)算得上是另起爐灶,而lock free算得上是就地解決吧。所以各種方案其實也不矛盾,都是為人民服務嘛;)

個人對lock free的觀點是這項技術不應該也不會大面積地應用在實際程式設計中,畢竟像這種高難度的東西還是有點曲高和寡。對於技術本身反正是見仁見智,愛用就用,不用拉倒唄。不過我想無論是否在實際當中使用lock free技術,瞭解和研究這項技術本身都會對理解並行程式設計有很大的幫助。

lock free的基礎是CAS (Compare And Swap)函式,它的功能可以用如下的程式碼描述:

如果以前沒有真正瞭解過lock free技術,可能會產生疑惑,這個函式對解決我們並行中的競爭問題能有什麼幫助呢?如果你有這樣的疑問,沒問題,因為我第一次見到這個的時候也是一頭霧水。不過實際上這個函式只是描述了Compare And Swap的執行過程,函式本身並不能

直接被使用,只是虛擬碼描述而已,切記。不過現代處理器通常都實現了對應CAS功能的原子指令,比如x86彙編裡面的“ CMPXCHG ”就提供了這樣的功能,所以CAS的實現實際是平臺相關的。文章裡面給出了linux下利用asm的實現,相較之下windows上面的實現可以簡單一些,因為windows提供了一族以InterlockedCompareExchange開頭的API來封裝具體的實現,比如其中的InterlockedCompareExchange函式宣告如下:

LONG

InterlockedCompareExchange(

    IN OUT PLONG  Destination,

    IN LONG  Exchange,

    IN LONG  Comparand

);

這裡值得注意的是函式的返回值是原始的*Destination內容,並不是像上面的C++程式碼描述的那樣會直接返回一個布林值指示交換操作是否真正發生。所以返回值的工作必須由我們自己來完成。顯然這裡不能在函式開始處對*Destination和Comparand進行比較然後用if/else這樣的分支來選擇返回true還是false,因為那樣的話就必須有一個lock來進行保護了。我們好不容易找到個辦法來避免對lock的依賴,豈能又給繞回去了?這裡的標準做法是用API呼叫的返回值與Comparand傳入引數進行比較,因為API確保返回的是一個LONG型別的值物件,這個值始終都是存在於函式的棧上面,所以即便在比較之前發生中斷並且實際的*Destination內容又被其它執行緒修改了,也並不影響此處的比較結果,當然也不會影響CAS函式的返回值了。程式碼實現如下:

當然針對實際呼叫中T通常為指標的情況可以直接用InterlockedCompareExchangePointer來避免顯示的型別轉換,可以考慮再加一個偏特化的template,不過處理方法一樣。

最近發現codeproject上有一篇文章分別用C++和C#實現了lock free的演算法,不過很遺憾這個實現是有問題的。由此也可以說明並行程式設計特別是lock free確實不是一件容易的事情,連這樣的文章都弄錯了。看一下它的CAS函式實現:

這裡的問題是函式裡面用了兩條語句來完成對目標物件的修改,雖然兩條語句本身都是atomic的,不過在它們中間仍然可能發生中斷,所以這個CAS函式並沒有發揮預期的作用。實際上基於CAS語句的lock free技術的本質是對於任何資料的修改並不直接修改物件本身,而是先去修改目標物件的一份拷貝(copy),然後通過實現為atomic的一次交換操作將修改後的拷貝內容賦值給目標物件。所以CAS語句通常像下面這樣使用:

其實類似的思路也應用到解決其它問題。如果還沒想到點什麼的話可以去找一份C++裡面智慧指標(smart pointer)的程式碼來看看,所以這其實也是異常安全(exception safe)編碼的必備武器之一。

上面的copy操作效率比較低,所以牛牛們在具體應用中想出了各種方法來減小資料copy的粒度。不過無論如何,將CAS語句實現成多條需要讀寫原始dest資料的操作都是不正確的。此處一種可行的做法是使用類似InterlockedCompareExchange64之流的加強版CAS API來一石二鳥,具體實現上還有很多巧妙的解法,已超出本文的範疇,就此打住。

題外話:如果之前對異常安全(exception safety)編碼有所瞭解,可能會發現和本文談的lock free有相似的地方。因為在異常安全裡面對資源的修改最好的方式並不是直接修改目標物件本身,也是先建立/修改一份副本物件,最後通過保證沒有異常丟擲的swap操作來修改目標物件的內容。不瞭解的話可以隨便找一份智慧指標(smart pointer,比如boost裡面的shared_ptr)的原始碼l瞧瞧裡面是不是用到了很多次std::swap().

lock free為的是執行緒訪問的安全,exception safety為的是保證在丟擲異常情況下資料的安全,不過解決問題的基本思路卻是一樣的。