-
Volatile變量
鎖定
在程序設計中,尤其是在C語言、C++、C#和Java語言中,使用volatile關鍵字聲明的變量或對象通常具有與優化、多線程相關的特殊屬性。通常,volatile關鍵字用來阻止(偽)編譯器認為的無法“被代碼本身”改變的代碼(變量/對象)進行優化。如在C語言中,volatile關鍵字可以用來提醒編譯器它後面所定義的變量隨時有可能改變,因此編譯後的程序每次需要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數據。如果沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,如果這個變量由別的程序更新了的話,將出現不一致的現象。
在C環境中,volatile關鍵字的真實定義和適用範圍經常被誤解。雖然C++、C#和Java都保留了C中的volatile關鍵字,但在這些編程語言中volatile的用法和語義卻大相徑庭。
- 中文名
- Volatile變量
- 語 種
- C語言,C++,JAVA
- 特 點
- 優化、多線程相關的特殊屬性
目錄
Volatile變量C和C++中的volatile
在C,以及C++中,volatile關鍵字的作用:
- 允許訪問內存映射設備
- 允許在setjmp和longjmp之間使用變量
- 允許在信號處理函數中使用sig_atomic_t變量
根據相關的標準(C,C++,POSIX,WIN32)和絕大多數實現,對volatile變量的操作並不是原子的,也不能用來為線程建立嚴格的happens-before關係。volatile關鍵字就像便攜式線程構建一樣基本沒什麼用處。
Visual C++2005 保證volatile變量是一種內存屏障,阻止編譯器和CPU重新安排讀入和寫出語義。在先前版本的Visual C++則沒有此類保證。在其他方面將指針定義為volatile可能會影響程序的性能。例如,如果指針定義對代碼的其他地方可見,強制編譯器將指針視為屏障,就會降低程序的性能,這是完全不必要的。
[1]
對用户定義的非基本數據類型使用volatile
基本類型的對象用volatile修飾後,仍舊支持所有的操作(加、乘、賦值等)。但是,用户定義的非基本類型(class、struct、union)的對象被volatile修飾後,具有不同行為:
- 只能調用volatile成員函數;即只能訪問它的接口的子集。
- 只能通過const_cast運算符轉為沒有volatile修飾的普通對象。即由此可以獲得對類型接口的完全訪問。
- 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的結構對象不被不受控制地訪問。
Volatile變量C語言中MMIO的例子
在這裏例子中,代碼將foo的值設置為0。然後開始不斷地輪詢它的值直到它變成255:
static int foo; void bar(void) { foo = 0; while (foo != 255) ; }
一個執行優化的編譯器會提示沒有代碼能修改foo的值,並假設它永遠都只會是0.因此編譯器將用類似下列的無限循環替換函數體:
void bar_optimized(void) { foo = 0; while (true) ; }
但是,foo可能指向一個隨時都能被計算機系統其他部分修改的地址,例如一個連接到中央處理器的設備的硬件寄存器,上面的代碼永遠檢測不到這樣的修改。如果不使用volatile關鍵字,編譯器將假設當前程序是系統中能改變這個值部分(這是到最廣泛的一種情況)。 為了阻止編譯器像上面那樣優化代碼,需要使用volatile關鍵字:
static volatile int foo; void bar (void) { foo = 0; while (foo != 255) ; }
這樣修改以後循環條件就不會被優化掉,當值改變的時候系統將會檢測到。
Volatile變量C語言中的優化對比
下面的C程序和後面的彙編代碼展示了volatile關鍵字如何影響編譯器的輸出。這裏使用的編譯器是GCC。
Volatile變量Java中的volatile
Java也支持volatile關鍵字,但它被用於其他不同的用途。當volatile用於一個作用域時,Java保證如下:
- (適用於Java所有版本)讀和寫一個volatile變量有全局的排序。也就是説每個線程訪問一個volatile作用域時會在繼續執行之前讀取它的當前值,而不是(可能)使用一個緩存的值。(但是並不保證經常讀寫volatile作用域時讀和寫的相對順序,也就是説通常這並不是有用的線程構建)。
使用volatile會比使用鎖更快,但是在一些情況下它不能工作。volatile使用範圍在Java5中得到了擴展,特別是雙重檢查鎖定能夠正確工作。