我相信你一定遇到這樣的問題:你把某人的名字和電話号碼寫到你的通訊錄中,數個月之後企圖打電話給這個人,卻發現資料已經過期了。同樣的情況也可能發生在編譯器為你産生的程式代碼中。
編譯器最優化的結果是,設法把常用的資料放在CPU的内部寄存器中,這些寄存器就像你的通訊錄一樣,資料從寄存器中讀出,遠比從記憶體中讀出快得多,就好像使你從你的通訊錄中讀資料,遠比從大電話簿中讀資料要快得多,當然,如果另一個線程改變了記憶體的變量值,那麼此變量在寄存器中的拷貝值就算是“過期”了。
在一個單線程中,這種情況不可能發生,編譯器可以分析你的程式的每一個操作,然後確定資料在适當時候會被重新載入。然而在一個多線程中就不可能知道其他線程在做什麼,是以編譯器一定不能夠允許讓一個共享變量的值拷貝到寄存器中。
C和C++有一個鮮為人知的關鍵字,教導編譯器如何在一個variable-by-variable 的基礎上采取行動,這個關鍵字是volatile。這個關鍵字告訴編譯器不要持有變量的臨時性拷貝,它可以适用于基礎類型,如int或long,也适用于一整個C結構或C++類。後面這種情況下,結構或類的所有成員都會被視為volatile。
使用volatile并不會否定critical sections或mutexes的需要。
例如你說:
a = a + 3
還是會有一小段時間,a會被放在一個寄存器中,因為算數運算隻能夠在寄存器中進行。一般而言,volatile關鍵字适用于行與行之間,而不是放在行内。
讓我們看一個非常簡單的函數,觀察編譯器制造出來的彙編語言中的瑕疵,并看看volatile如何修正這個瑕疵。這個範例函數就是一個busy loop
,雖然不建議寫busy loop,但是它卻是解釋這裡的觀念的一個最好的例子。本例之中,WaitForKey()等待一個字元的到來:
void WaitForKey(char* pch)
{
while ( *pch == 0 )
;
}
當你把最優化選項都關閉後,編譯這個程式,獲得以下結果,進入點和退出點已經被我移除(為了讓程式代碼清爽一些),粗體字代表C源代碼。
; while(*pch == 0)
$L27:
; Load the address stored in pch
mov eax, DWORD PTR _pch$[ebp]
; Load the character into the EAX register
movsx eax,BYTE PTR _pch$[eax]
;Compare the value to zero
test eax, eax
; If not zero, exit loop
jne $L28
; ;
jmp $L27
$L28:
; }
這個未曾優化的函數代碼不斷地載入适當的位址,載入位址中的内容,測試其結果,慢,但是準确,此版本在多線程程式上沒有問題。
現在我們看看最優化帶來什麼影響:
; {
; Load the address stored in pch
mov eax, DWORD PTR _pch$[esp - 4]
; Load the character into the AL register
movsx al, BYTE PTR [eax]
; while (*pch == 0)
$L84:
; Compare the value in the AL register to zero
test al, al
; If still zero, try again
je SHORT $L84
; ;
; }
短多了,最優化果然有用。但是請注意,編譯器把MOV指令放到循環之外,這個操作稱為 loop-invariant removal。這個在單線程程式中應該是一個很好地最優化,但是在多線程程式中,如果另一個線程改變了數值,則循環永遠不會結束。被測試的值永遠被放在寄存器中,很明顯那是一隻“臭蟲”。
解決方法就是重寫WaitForKey(),把參數pch聲明為 volatile:
void WaitForKey (volatile char* pch)
{
while (* pch == 0 )
;
}
這項改變對于非最優化的版本沒有影響,但請看看最優化後的結果:
; {
; Load the address stored in pch
mov eax, DWORD PTR _pch$[esp-4 ]
; while (*pch == 0)
$L84:
; Directly compare the value to zero
cmp BYTE PTR [eax], 0
; If still zero, try again
je SHORT $L84
; ;
; }
這個版本幾乎完美,位址不會改變,是以位址聲明被移到循環之外,位址内容是volatile,是以每次循環之中它不斷地被重新檢查。
精細地說,把一個const volatile 變量傳給函數作為參數是合法的,如此的聲明意味着函數不能夠改變變量的值,但是變量的值卻可以被另一個線程在任何時間改變掉。
const 和 volatile 都是ANSI的标準關鍵字,所有的C/C++編譯器都應該有支援。