1. 程式人生 > >對volatile int的單寫多讀??

對volatile int的單寫多讀??

我已經不止一次聽到關於volatile int的安全讀寫方面的謬論了。

最常見的是所謂的volatile int的變數的單寫多讀操作是多執行緒安全的。

這個結論很搞笑,因為每當支援這個節論的人給我講一大堆諸如鎖匯流排,原子指令方面的東西時,(權且當他們都說的全對)我幾乎都可以反問他:根據你說的東西可以得到一個結論,volatile int是也多執行緒寫安全的(序列的),volatile int就是原生的訊號量。那些搞併發程式設計的人難道都是吃飽了撐的,搞什麼鎖來玩的?

好了,談些基本點,基於c語言標準:

1。volatile的行為是不可移植的(基於編譯器,編譯器基於cpu)。c語言另外一個不可移植的東西是位域。

2。volatile的行為編譯器至少都當作io行為(最低限度的可移植語義,也是最好理解的一種說法,可惜很多人都不明白io行為意味著什麼)。

3。volatile變數的讀寫操作將禁止該操作後的指令在該操作之前執行,之前的指令也不得推後執行。(禁止執行流水線,向量化等等常見優化,總之跨volatile不能打亂,合併原有的語句來進行優化)

4。c程式碼編譯成彙編之後,什麼volatile之內的關鍵字全沒了,只有一堆彙編指令。

5。多執行緒的語義取決於實現(基於作業系統,作業系統基於cpu,實現程式碼也都是彙編)。不同的作業系統的多執行緒實現和排程可能基於不同的原理,和記憶體管理機制往往關係也不小。這也是win32居然有使用者態的critical section這樣的同步原語供大家使用,因為它專著x86而linux需要跨cpu,前者就可以直接封裝i386彙編來玩,而且恰好win32的執行緒可以不用進核心態進行系統呼叫就休眠,喚醒自己(這就是原生多執行緒系統的好)。

6。c語言的標準和多執行緒程式設計沒有任何關係,c語言不保證程式碼在多執行緒中執行的情況。c的標準出現的可比最早的多執行緒作業系統win和solary早多了,不可能預測生後事。不同的系統多執行緒實現更是千奇百怪,很難提煉出一個公有的底層特性。你看看java虛擬機器吧,支援直接的單寫多讀嗎?就算某個分時系統的多執行緒很弱智,也不能保證別的作業系統也能那麼弱智。

7。現有的多執行緒的實現和編譯器的volatile實現的組合幾乎都不能保證:在一個執行緒中的寫操作的順序,在別的執行緒看起來是不變的!!

7是什麼意思呢?下面是一個雙核 x86cpu的例子

int d0 = 0;

int d1 = 1;

volatile int x0 = 0;

volatile int x1 = 1;

x1 = d0;

x0 = d1;

生成的程式碼會按d0,d1,x0,x1的順序執行,其中d0,d1的順序可以調換。

然而出現了thread2,恰好根據win32的執行緒模型,單個cpu可以獨自管一個執行緒。好,另外一個核心負責執行thread2.

此時thread2所在的核心需要訪問在thread1中最近有寫行為的記憶體段x0,x1,根據一些要求,由於跨了cpu(核),這可能會觸發一個記憶體重新整理的行為,使得cpu1最近發生的記憶體讀寫行為能夠同步到cpu2中。這裡背後的勾當很複雜,老p4雙核和酷睿雙核的處理差別巨大。當年amd與intel的真假雙核之爭告訴我們,同樣是x86,amd和intel有時也是有區別的。

假設thread2有下列程式碼要執行

while(x0 == 1)printf("%d/n",x1);//直到x0 == 1,才打印x1。此時似乎x1已經必然為0。

如果打印出1來請千萬不要奇怪。雖然這個例子我確實不能打印出1來(我找不到這樣的編譯器),但是這樣的行為是符合標準的。

!!原因:

thread2在試圖把thread1中的執行過的彙編寫指令x1 = d0;x0 = d1;執行一遍以保證相關的記憶體資料一致時(原因見上),赫然發現:這段彙編指令寫操作居然先寫高地址的x1 ,再寫低地址的x0,對於x86這是一個低效行為;而且x0和x1的寫操作交換次序後沒有任何副作用。此時volatile已經消失,thread2眼裡只有彙編指令,可以任意的改變這些指令的執行順序。

好,thread2把x1 = d0;x0 = d1;交換了位置準備進行執行。現在需要執行的是x0 = d1;x1 = d0;和while(x0==1)printf("%d/n",x1);

此時thread2又開始騷動了:對於相近的記憶體進行讀寫,速度會提升(提升cache命中率,減少缺頁中斷等等)。支援亂序指令執行的cpu2又開始優化了:while(x0==1)printf("%d/n",x1);這2段程式碼可以任意的插入x0 = d1;x1 = d0;中去(因為這本質是cpu1的指令,不同的cpu執行指令的順序是沒法假設的)。原有指令的訪問順序是x0,x1,x0,x1,交錯進行訪問,把對x0和x1的訪問集中些,改為x0,x0,x1,x1,顯然速度會快些。

於是最終在cpu2執行的指令可能是

x0 = d1;

while(x0==1)

printf("%d/n",x1);

x1 = d0;

//可能輸出1

注意cpu2交換了次序之後,他知道printf("%d/n",x1);的結果可能會變化,但是這個變化本質是由於cpu1和cpu2執行指令的相互間順序變化造成的,而不是cpu2自己(原本)要執行的指令順序變化造成的。cpu2不會顧忌這些的。也就是說,我們這裡的單寫多讀行為,是可能造成不確定的結果的,和那些沒有加鎖的多執行緒程式錯的原因是一樣的。x0,x1的值沒有錯,不是"髒"資料(這往往是那些鼓吹volatile int的變數的單寫多讀操作是多執行緒安全的人最愛強調的一點),問題是程式的輸出卻錯了。這樣問題的出現,debug肯定不成(debug禁用一切優化),看反彙編你又無法獲取執行緒環境來對應不同cpu的暫存器,因此我打賭不可能有人能夠檢查出來問題的緣由。有寫重症患者也許會精神崩潰:自己信賴了一輩子的編譯器難道有bug?我崇拜的上帝居然是偽君子?......

明白了這裡之後,你還敢假設volatile int的單寫多讀安全嗎?絕大多數人都把c語言想的太底層了,然而c語言也是抽象;volatile包括一些c語言抽象出來的機器模型之外的概念,但是受制於編譯過程,依然不能完美包含各個cpu各個作業系統的方方面面。現有的多執行緒庫的同步原語背後都是彙編!!

一句話,跨執行緒的時候volatile不能限制多cpu的指令的重新排列。這僅僅是冰山一角。

也許你的程式碼暫時不依賴這些指令的順序,問題是誰能保證以後呢?vc6的string多執行緒時在雙核機上面崩潰的行為,不就是一個經典的教訓嗎?

當然有時候有些辦公室哲學也會產生影響力.DCLP(Double-Checked Loking Pattern)已經被證明是錯誤的,但是我還沒聽說過這個錯誤在實踐中導致錯誤的執行結果或者崩潰(ace用它來初始化靜態物件很久了)。所以專案組長拿著這樣的程式碼叫人重寫時,絕大多數人都會懶洋洋的說,“這段程式碼我們都跑了這麼多年了,從來沒有出過問題。能用就繼續用吧”。我相信很多人也是這個原因而堅持反對本文的內容的。