Volatile變量

在程序設計中，尤其是在C語言、C++、C#和Java語言中，使用volatile關鍵字聲明的變量或對象通常具有與優化、多線程相關的特殊屬性。通常，volatile關鍵字是用來阻止（偽）編譯器因誤認某段程式碼無法被程式碼本身所改變，而造成的過度優化。如在C語言中，volatile關鍵字可以用來提醒編譯器它後面所定義的變量隨時有可能改變，因此編譯後的程序每次需要存儲或讀取這個變量的時候，都會直接從變量地址中讀取數據。如果沒有volatile關鍵字，則編譯器可能優化讀取和存儲，可能暫時使用寄存器中的值，如果這個變量由別的程序更新了的話，將出現不一致的現象。

在C環境中，volatile關鍵字的真實定義和適用範圍經常被誤解。雖然C++、C#和Java都保留了C中的volatile關鍵字，但在這些程式語言中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可能會影響程序的性能。例如，如果指針定義對代碼的其他地方可見，強制編譯器將指針視為屏障，就會降低程序的性能，這是完全不必要的。

### 對使用者定義的非基本數據類型使用volatile

基本類型的對象用volatile修飾後，仍舊支持所有的操作（加、乘、賦值等）。但是，使用者定義的非基本類型（class、struct、union）的對象被volatile修飾後，具有不同行為：

- 只能調用volatile成員函數；即只能訪問它的接口的子集。
- 只能通過const_cast運算符轉為沒有volatile修飾的普通對象。即由此可以獲得對類型接口的完全訪問。
- volatile性質會傳遞給它的數據成員。

### volatile與多線程語義

臨界區內部，通過互斥鎖（mutex）保證只有一個線程可以訪問，因此臨界區內的變量不需要是volatile的；而在臨界區外部，被多個線程訪問的變量應為volatile，這也符合了volatile的原意：防止編譯器緩存（cache）了被多個線程並發用到的變量。volatile對象只能調用volatile成員函數，這意味著應僅對多線程並發安全的成員函數加volatile修飾，這種volatile成員函數可自由用於多線程並發或者重入而不必使用臨界區；非volatile的成員函數意味著單線程環境，只應在臨界區內調用。在多線程編程中可以令該數據對象的所有成員函數均為普通的非volatile修飾，從而保證了僅在進入臨界區（即獲得了互斥鎖）後把該對象顯式轉為普通對象之後才能調用該數據對象的成員函數。這種用法避免了編程者的失誤——在臨界區以外訪問共享對象的內容：

```cpp
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`：

```c
static int foo;

void bar(void) {
    foo = 0;

    while (foo != 255)
         ;
}
```

一個執行優化的編譯器會提示沒有代碼能修改`foo`的值，並假設它永遠都只會是`0`.因此編譯器將用類似下列的無限循環替換函數體：

```c
void bar_optimized(void) {
    foo = 0;

    while (true)
         ;
}
```

但是，foo可能指向一個隨時都能被電腦系統其他部分修改的地址，例如一個連接到中央處理器的設備的硬體寄存器，上面的代碼永遠檢測不到這樣的修改。如果不使用volatile關鍵字，編譯器將假設當前程序是系統中唯一能改變這個值部分（這是到目前為止最廣泛的一種情況）。

為了阻止編譯器像上面那樣優化代碼，需要使用volatile關鍵字：

```c
static volatile int foo;

void bar (void) {
    foo = 0;

    while (foo != 255)
        ;
}
```

這樣修改以後循環條件就不會被優化掉，當值改變的時候系統將會檢測到。

### C語言中的優化對比

下面的C程序和後面的彙編代碼展示了`volatile`關鍵字如何影響編譯器的輸出。這裡使用的編譯器是GCC。

彙編對照

不使用volatile｜使用volatile

```c
#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;
}
```

```c
#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

```asm
    .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]"
```

```asm
	.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關係，類似於申請和釋放一個互斥鎖。

使用`volatile`會比使用鎖更快，但是在一些情況下它不能工作。`volatile`使用範圍在Java5中得到了擴展，特別是雙重檢查鎖定現在能夠正確工作。

## Ada中的volatile

在Ada中，比起關鍵字，`Volatile`標記更像是一種指令。「對於volatile對象而言，所有讀和更新都會作為一個整體直接執行到記憶體」。

## 外部連結

- Ada Reference Manual C.6: Shared Variable Control