天天看點

Win32多線程之volatile

   我相信你一定遇到這樣的問題:你把某人的名字和電話号碼寫到你的通訊錄中,數個月之後企圖打電話給這個人,卻發現資料已經過期了。同樣的情況也可能發生在編譯器為你産生的程式代碼中。

   編譯器最優化的結果是,設法把常用的資料放在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++編譯器都應該有支援。