Volatile變數
在程式設計中,尤其是在C語言、C++、C#和Java語言中,使用volatile關鍵字聲明的變數或對象通常具有與最佳化、多執行緒相關的特殊屬性。通常,volatile關鍵字是用來阻止(偽)編譯器因誤認某段程式碼無法被程式碼本身所改變,而造成的過度優化。如在C語言中,volatile關鍵字可以用來提醒編譯器它後面所定義的變數隨時有可能改變,因此編譯後的程式每次需要儲存或讀取這個變數的時候,都會直接從變數位址中讀取資料。如果沒有volatile關鍵字,則編譯器可能最佳化讀取和儲存,可能暫時使用暫存器中的值,如果這個變數由別的程式更新了的話,將出現不一致的現象。
在C環境中,volatile關鍵字的真實定義和適用範圍經常被誤解。雖然C++、C#和Java都保留了C中的volatile關鍵字,但在這些程式語言中volatile的用法和語意卻大相逕庭。
C和C++中的volatile
在C,以及C++中,volatile關鍵字的作用[1]:
- 允許訪問主記憶體對映裝置
- 允許在
setjmp
和longjmp
之間使用變數 - 允許在訊號處理常式中使用sig_atomic_t變數
根據相關的標準(C,C++,POSIX,WIN32)和目前絕大多數實現,對volatile變數的操作並不是原子的,也不能用來為執行緒建立嚴格的happens-before關係。volatile
關鍵字就像可攜式執行緒構建一樣基本沒什麼用處[1][2][3][4][5]。
Visual C++ 2005 保證volatile變數是一種主記憶體屏障,阻止編譯器和CPU重新安排讀入和寫出語意。[6] 在先前版本的Visual C++則沒有此類保證。在其他方面將指標定義為volatile可能會影響程式的效能。例如,如果指標定義對代碼的其他地方可見,強制編譯器將指標視為屏障,就會降低程式的效能,這是完全不必要的。
對使用者定義的非基本資料類型使用volatile
基本類型的對象用volatile修飾後,仍舊支援所有的操作(加、乘、賦值等)。但是,使用者定義的非基本類型(class、struct、union)的對象被volatile修飾後,具有不同行為:
- 只能呼叫volatile成員函式;即只能訪問它的介面的子集。
- 只能通過const_cast運算子轉為沒有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語言中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)
;
}
這樣修改以後迴圈條件就不會被最佳化掉,當值改變的時候系統將會檢測到。
C語言中的最佳化對比
下面的C程式和後面的組譯代碼展示了volatile
關鍵字如何影響編譯器的輸出。這裡使用的編譯器是GCC。
組譯對照 | |
---|---|
不使用volatile | 使用volatile |
#include <stdio.h>
int main() {
int a = 10, b = 100, c = 0, d = 0;
printf("%d", a + b);
a = b;
c = b;
d = b;
printf("%d", c + d);
return 0;
}
|
#include <stdio.h>
int main() {
volatile int a = 10, b = 100, c = 0, d = 0;
printf("%d", a + b);
a = b;
c = b;
d = b;
printf("%d", c + d);
return 0;
}
|
gcc -O3 -S without.c -o without.s | gcc -S with.c -o with.s |
.file "without.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
movl $110, 4(%esp)
movl $.LC0, (%esp)
call printf
movl $200, 4(%esp)
movl $.LC0, (%esp)
call printf
addl $20, %esp
xorl %eax, %eax
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (GNU) 4.2.1 20070719 [FreeBSD]"
|
.file "with.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $36, %esp
movl $10, -8(%ebp)
movl $100, -12(%ebp)
movl $0, -16(%ebp)
movl $0, -20(%ebp)
movl -8(%ebp), %edx
movl -12(%ebp), %eax
movl $.LC0, (%esp)
addl %edx, %eax
movl %eax, 4(%esp)
call printf
movl -12(%ebp), %eax
movl %eax, -8(%ebp)
movl -12(%ebp), %eax
movl %eax, -16(%ebp)
movl -12(%ebp), %eax
movl %eax, -20(%ebp)
movl -16(%ebp), %edx
movl -20(%ebp), %eax
movl $.LC0, (%esp)
addl %edx, %eax
movl %eax, 4(%esp)
call printf
addl $36, %esp
xorl %eax, %eax
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (GNU) 4.2.1 20070719 [FreeBSD]"
|
Java中的volatile
Java也支援 volatile
關鍵字,但它被用於其他不同的用途。當 volatile
用於一個作用域時,Java保證如下:
- (適用於Java所有版本)讀和寫一個
volatile
變數有全域的排序。也就是說每個執行緒訪問一個volatile
作用域時會在繼續執行之前讀取它的當前值,而不是(可能)使用一個快取的值。(但是並不保證經常讀寫volatile
作用域時讀和寫的相對順序,也就是說通常這並不是有用的執行緒構建)。 - (適用於Java5及其之後的版本)
volatile
的讀和寫建立了一個happens-before關係,類似於申請和釋放一個互斥鎖[7]。
使用volatile
會比使用鎖更快,但是在一些情況下它不能工作。volatile
使用範圍在Java5中得到了擴充,特別是雙重檢查鎖定現在能夠正確工作[8]。
Ada中的volatile
在Ada中,比起關鍵字,Volatile
標記更像是一種指令。「對於volatile對象而言,所有讀和更新都會作為一個整體直接執行到主記憶體」[9]。
參考
- ^ 1.0 1.1 Publication on C++ standards committee website; http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html (頁面存檔備份,存於網際網路檔案館)
- ^ Volatile Keyword In Visual C++; http://msdn2.microsoft.com/en-us/library/12a04hfd.aspx (頁面存檔備份,存於網際網路檔案館)
- ^ Linux Kernel Documentation - Why the "volatile" type class should not be used; 存档副本. [2007-08-17]. (原始內容存檔於2007-08-25).
- ^ Volatile: Almost Useless for Multi-Threaded Programming (Intel Software Network); 存档副本. [2011-08-31]. (原始內容存檔於2007-12-13).
- ^ C++ and the Perils of Double-Checked Locking; http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf (頁面存檔備份,存於網際網路檔案館)
- ^ 存档副本. [2016-02-20]. (原始內容存檔於2012-10-20).
- ^ Section 17.4.4: Synchronization Order The Java Language Specification, 3rd Edition. Sun Microsystems. 2005 [2010-11-22]. (原始內容存檔於2012-02-14).
- ^ Neil Coffey. Double-checked Locking (DCL) and how to fix it. Javamex. [2009-09-19]. (原始內容存檔於2021-03-06).
- ^ "C.6 Shared Variable Control" Ada Reference Manual. ISO. 2005 [2010-05-04]. (原始內容存檔於2021-03-06).