1. 程式人生 > >volatile型別修飾符

volatile型別修飾符

volatile是一個型別修飾符,其作用是作為指令關鍵字,確保本條指令不會因為編譯器的優化而省略,且要求每次直接讀值。簡單來說,就是防止編譯器對程式碼進行優化。
eg:
a[1]=1;
a[1]=2;
a[1]=3;
a[1]=4;
對於外部硬體而言,上述四條語句分別表示不同的操作,會產生不同的動作,但是編譯器卻會對上述四條語句進行優化,認為只有a[1]=4有效;如果加入關鍵字volatile,則編譯器會逐一的進行編譯併產生相應的機器程式碼。


經常使用到volatile變數的例子:
(1)並行裝置的硬體暫存器(如狀態暫存器);
(2)一箇中斷服務子程式中會訪問到的非自動變數;
(3)多執行緒應用中被幾個任務共享的變數;
嵌入式系統程式經常同硬體、中斷、RTOS等等打交道,所有這些都要求使用volatile變數。


如在C語言中,volatile關鍵字可以用來提醒編譯器它後面所定義的變數隨時有可能改變,因此編譯後的程式每次需要儲存或讀取這個變數的時候,都會直接從變數地址中讀取資料。如果沒有volatile關鍵字,則編譯器可能優化讀取和儲存,可能暫時使用暫存器中的值,如果這個變數由別的程式更新了的話,將出現不一致的現象。

volatile與多執行緒語義
臨界區內部,通過互斥鎖(mutex)保證只有一個執行緒可以訪問,因此臨界區內的變數不需要是volatile的;而在臨界區外部,被多個執行緒訪問的變數應為volatile,這也符合了volatile的原意:防止編譯器快取(cache)了被多個執行緒併發用到的變數。volatile物件只能呼叫volatile成員函式,這意味著應僅對多執行緒併發安全的成員函式加volatile修飾,這種volatile成員函式可自由用於多執行緒併發或者重入而不必使用臨界區;非volatile的成員函式意味著單執行緒環境,只應在臨界區內呼叫。在多執行緒程式設計中可以令該資料物件的所有成員函式均為普通的非volatile修飾,從而保證了僅在進入臨界區(即獲得了互斥鎖)後把該物件顯式轉為普通物件之後才能呼叫該資料物件的成員函式。這種用法避免了程式設計者的失誤——在臨界區以外訪問共享物件的內容:

template <typename T> class LockingPtr{
  public:
    LockingPtr(volatile T& obj, Mutex& mtx)
        :pObj_(const_cast<T*>(&obj) ),  pMtx_(&mtx)
        {  mtx.Lock();  }
    ~LockingPtr()
        { pMtx->Unlock();  }
    T& operator*()
        {  return *pObj_;
} T* operator->() { return pObj_; } private: T* pObj_; Mutex* pMtx_; LockingPtr(const LockingPtr&); LockingPtr& operator=(const LockingPtr&); }

對於內建型別,不應直接用volatile,而應把它包裝為結構的成員,就可以保護了volatile的結構物件不被不受控制地訪問.


C語言的例子:

static int foo;
  
void bar(void) {
    foo = 0;
  
    while (foo != 255)
         ;
}

在這裡例子中,程式碼將foo的值設定為0。然後開始不斷地輪詢它的值直到它變成255.

void bar_optimized(void) {
    foo = 0;
  
    while (true)
         ;
}

一個執行優化的編譯器會提示沒有程式碼能修改foo的值,並假設它永遠都只會是0.因此編譯器將用類似下列的無限迴圈替換函式體.

static volatile int foo;
  
void bar (void) {
    foo = 0;
  
    while (foo != 255)
        ;
}

但是,foo可能指向一個隨時都能被計算機系統其他部分修改的地址,例如一個連線到中央處理器的裝置的硬體暫存器,上面的程式碼永遠檢測不到這樣的修改。如果不使用volatile關鍵字,編譯器將假設當前程式是系統中能改變這個值部分(這是到最廣泛的一種情況)。 為了阻止編譯器像上面那樣優化程式碼,需要使用volatile關鍵字,這樣修改以後迴圈條件就不會被優化掉,當值改變的時候系統將會檢測到。