1. 程式人生 > 其它 >C++11中的原子量和記憶體序詳解

C++11中的原子量和記憶體序詳解

轉載來自:https://www.jb51.net/article/141896.htm

一、多執行緒下共享變數的問題

在多執行緒程式設計中經常需要在不同執行緒之間共享一些變數,然而對於共享變數操作卻經常造成一些莫名奇妙的錯誤,除非老老實實加鎖對訪問保護,否則經常出現一些(看起來)匪夷所思的情況。比如下面便是兩種比較“喜聞樂見”的情況。

(a) i++問題

在多執行緒程式設計中,最常拿來舉例的問題便是著名的i++ 問題,即:多個執行緒對同一個共享變數i執行i++ 操作。這樣做之所以會出現問題的原因在於i++這個操作可以分為三個步驟:

stepoperation
1 i->reg(讀取i的值到暫存器)
2 inc-reg(在暫存器中自增i的值)
3 reg->i (寫回記憶體中的i)

上面三個步驟中間是可以間隔的,並非原子操作,也就是說多個執行緒同時執行的時候可能出步驟的交叉執行,例如下面的情況:

stepthread Athread B
1 i->reg
2 inc-reg
3 i->reg
4 inc-reg
5 reg->i
6 reg->i

假設i一開始為0,則執行完第4步後,在兩個執行緒都認為暫存器中的值為1,然後在第5、6兩步分別寫回去。最終兩個執行緒執行完成後i的值為1。但是實際上我們在兩個執行緒中執行了i++,原本希望i的值為2。i++ 實際上可以代表多執行緒程式設計中由於操作不是原子的而引發的交叉執行這一類的問題,但是在這裡我們先只關注對單個變數的操作。

(b)指令重排問題

有時候,我們會用一個變數作為標誌位,當這個變數等於某個特定值的時候就進行某些操作。但是這樣依然可能會有一些意想不到的坑,例如兩個執行緒以如下順序執行:

stepthread Athread B
1 a = 1
2 flag= true
3 if flag== true
4 assert(a == 1)

當B判斷flag為true後,斷言a為1,看起來的確是這樣。那麼一定是這樣嗎?可能不是,因為編譯器和CPU都可能將指令進行重排(編譯器不同等級的優化和CPU的亂序執行)。實際上的執行順序可能變成這樣:

stepthread Athread B
1 flag = true
2 if flag== true
3 assert(a == 1)
4 a = 1

這種重排有可能會導致一個執行緒內相互之間不存在依賴關係的指令交換執行順序,以獲得更高的執行效率。比如上面:flag 與 a 在A執行緒看起來是沒有任何依賴關係,似乎執行順序無關緊要。但問題在於B使用了flag作為是否讀取a的依據,A的指令重排可能會導致step3的時候斷言失敗。

解決方案

一個比較穩妥的辦法就是對於共享變數的訪問進行加鎖,加鎖可以保證對臨界區的互斥訪問,例如第一種場景如果加鎖後再執行i++ 然後解鎖,則同一時刻只會有一個執行緒在執行i++ 操作。另外,加鎖的記憶體語義能保證一個執行緒在釋放鎖前的寫入操作一定能被之後加鎖的執行緒所見(即有happens before 語義),可以避免第二種場景中讀取到錯誤的值。

那麼如果覺得加鎖操作過重太麻煩而不想加鎖呢?C++11提供了一些原子變數與原子操作來支援。