記憶體模型與原子操作

為何 volatile 不是原子性工具

/ volatile -> VOL-uh-tuhl /

volatile 關鍵字是 C 與 C++ 中最被誤解的字之一。一個普遍的民間信仰認為,把共享變數標上 volatile 就能讓它執行緒安全或變成原子。並非如此。volatile 與原子解決的是不同問題,把它們搞混就是一帖在 x86 上藏身、在 ARM 上現形的競爭配方。

volatile 實際的意思很狹窄:它告訴「編譯器」這個位置可能以它看不見的方式改變(一個記憶體映射的硬體暫存器、一個被同執行緒裡的信號處理常式修改的變數),所以編譯器不得把它快取進暫存器,且必須完全照寫地執行每一次讀與寫,並相對於「其他」volatile 存取保持原始碼順序。這就是它的全部職責。它對原子性什麼也沒說:在 32 位元機器上對一個 volatile 的 64 位元寫入,仍可能撕裂成兩半。它對跨執行緒排序什麼也沒說:volatile 不施加任何 acquire/release 語意、也不發出任何 CPU 記憶體屏障,所以硬體可自由地把一個 volatile 存取相對於普通記憶體重排,而另一核心可能看到陳舊資料。而且在 C/C++ 中,對一個 volatile 變數的資料競爭「仍然」是資料競爭,因而仍是未定義行為。

正確的工具劃出一條乾淨的界線。要在「單一」執行緒內與硬體或信號處理常式溝通,用 volatile(在 C11 是 volatile sig_atomic_t)。要在執行緒「之間」共享資料,用 _Atomic/std::atomic,它們保證原子性「並」讓你選擇 memory_order。(Java 與 C# 把水攪渾了,給了它們的 volatile acquire/release 語意——但 C、C++ 與 Rust 的 volatile「沒有」,而 Rust 甚至沒有 volatile 關鍵字,只有給記憶體映射輸入輸出用的 read_volatile/write_volatile 內建函式。)規則是:volatile 給你無法完全掌控的記憶體;atomic 給其他執行緒共享的記憶體。

volatile int counter; counter++; /* 不是原子:仍是載入、加、存回——兩個執行緒可能漏掉一次遞增 */ /* 正確: */ atomic_int counter; atomic_fetch_add(&counter, 1); /* 真正原子 */

volatile 阻止編譯器快取該值,但 counter++ 仍是三個可分割的步驟——只有原子 RMW 才安全。

在 C/C++/Rust 中,volatile 既不是原子、跨執行緒也不排序、更不能取代同步——它唯一正當的用途是記憶體映射輸入輸出與同執行緒的信號處理常式;別把它和 Java/C# 的 volatile 混為一談,那是另一回事。

又称
the volatile mythvolatile 迷思