Item 40:並行用 std::atomic,特殊記憶體用 volatile (Use std::atomic for concurrency, volatile for special memory)

Overview Table

面向 std::atomic<T> volatile T
用途 並行程式設計:多執行緒共享資料且不想用 mutex 特殊記憶體 (special memory):如 memory-mapped I/O
原子性 (atomicity) 保證:所有成員函式(含 RMW)對其他執行緒呈現原子性 完全不保證;多執行緒同時讀寫 = data race = UB
重排限制 (reordering) 保證:寫入 atomic 之前的程式碼不得被重排到其後(sequential consistency) 不限制編譯器或硬體重排
redundant loads / dead stores 編譯器優化掉(可存進暫存器) 編譯器不可優化掉,每次讀寫都必須執行
複製操作 copy ctor / copy assign 皆 = delete;也無 move 操作 可正常複製
實作方式 通常用特殊機器指令,比 mutex 更有效率 只是告訴編譯器「這塊記憶體不正常,別優化」
一句話總結:std::atomic並行工具volatile特殊記憶體工具。兩者解決的問題完全不同,甚至可以合用:volatile std::atomic<int>

std::atomic:原子操作與 data race

std::atomic<T> 的實例(如 std::atomic<int>std::atomic<bool>std::atomic<Widget*>)保證操作對其他執行緒看起來是原子的,行為如同置於 mutex 保護的 critical section 中,但通常以更高效的特殊機器指令實作。

std::atomic<int> ai(0);   // 初始化 ai 為 0
ai = 10;                  // 原子地設為 10
std::cout << ai;          // 原子地「讀取」ai 的值
++ai;                     // 原子地遞增為 11(RMW 操作)
--ai;                     // 原子地遞減為 10(RMW 操作)
// 其他執行緒只可能看到 0、10、11,不可能有其他值
std::cout << ai; 只保證「讀取 ai」這一步是原子的,整條敘述不是原子的——讀取後、輸出前,別的執行緒可能已改動 ai(但因 operator<< 以 by-value 收 int,輸出值仍是讀到的那個)。

volatile 版本則毫無保證++ 是 read-modify-write(讀→改→寫)三步操作,volatile 不保證其原子性。兩執行緒各遞增一次 volatile int vc(0),結果可能是 1 而非 2:

Thread 1                    Thread 2                vc
--------                    --------                --
read vc  → 0                                         0
                            read vc  → 0             0
0+1=1, write vc                                      1
                            0+1=1, write vc          1   ← 遞增兩次,結果卻是 1

而且這已構成 data race(同時讀寫既非 std::atomic 也無 mutex 保護的記憶體)→ undefined behavior,編譯器可生成任何程式碼,結果根本不可預測。std::atomic<int> ac 的最終值則必定是 2。

程式碼重排限制 (Code Reordering)

08-Concurrency-API/05-Void-Futures-For-Events 提到的「旗標通知」為例——第一個 task 算出值後以 flag 通知第二個 task:

std::atomic<bool> valAvailable(false);

auto imptValue = computeImportantValue();  // 計算重要的值
valAvailable = true;                       // 告知其他 task 值已就緒
上述重排保證只適用於循序一致性 (sequential consistency)——這是 std::atomic 的預設(也是本書語法唯一使用的)記憶體模型。C++11 也支援 relaxed(弱)一致性模型,允許更多重排、在某些硬體上更快,但極易寫錯、難以維護,連專家都常出錯;非必要應堅持使用預設的循序一致性。

volatile:特殊記憶體 (Special Memory)

「正常」記憶體的特性:寫入後值會保留到被覆寫為止。因此編譯器可做以下優化——消除 redundant loads(多餘讀取)dead stores(無效寫入)

int x;
auto y = x;   // 讀 x
y = x;        // 再讀一次 x   → 多餘,可刪
x = 10;       // 寫 x         → 從未被讀就被覆寫,可刪
x = 20;       // 再寫 x
// 編譯器可優化成:auto y = x;  x = 20;

(人類不會直接寫這種碼,但 template instantiation、inlining 與各種重排優化後,這類碼很常見。)

特殊記憶體(最常見:memory-mapped I/O,該位址實際連到感測器、顯示器、印表機、網路埠等周邊)不符合此假設:

volatile 就是告訴編譯器:「這塊記憶體上的操作,一律不准優化」——每次讀寫都必須真正執行:

volatile int x;    // 特殊記憶體
auto y = x;        // 讀 x
y = x;             // 再讀 x(不可優化掉)
x = 10;            // 寫 x(不可優化掉)
x = 20;            // 再寫 x
Pop quiz:auto y = x;xvolatile int)中 y 的型別是 int 而非 volatile int——auto 推導非參考非指標型別時會丟棄 const/volatile 修飾(見 02-Deducing-Types/02-Auto-Type-Deduction)。所以對 y 本身的多餘讀寫仍可被優化,但對 x 的兩次讀取都必須執行。

為何 std::atomic 不適合特殊記憶體:deleted copy 與 load/store

std::atomic複製操作被刪除= delete,見 04-Moving-To-Modern-Cpp/05-Deleted-Functions):硬體通常無法在單一原子操作中「讀 x 又寫 y」,所以 copy construction / copy assignment 不支援;move 操作也未宣告,依 04-Moving-To-Modern-Cpp/11-Special-Member-Function-Generation 的生成規則,std::atomic 既無 move construction 也無 move assignment。

std::atomic<int> x;
auto y = x;               // 錯誤!copy ctor 已刪除
y = x;                    // 錯誤!copy assign 已刪除

std::atomic<int> y(x.load());   // OK:原子地讀 x,再初始化 y
y.store(x.load());              // OK:原子地讀 x,再原子地寫 y

x.load() 與初始化/store分開的呼叫,整條敘述並非單一原子操作。而且編譯器可以x.load() 的結果存入暫存器、只讀 x 一次——這種優化對特殊記憶體是不可接受的(對 volatile 變數則不允許此優化)。這正是 std::atomic 不適合特殊記憶體、volatile 不適合並行的分工根源。

兩者可以合用

volatile std::atomic<int> vai;   // 操作既是原子的,也不可被優化掉
// 適用:被多執行緒並行存取的 memory-mapped I/O 位置
有些開發者偏好即使非必要也明寫 load/store,以在源碼中標示「這不是普通變數」——有助於辨識潛在的 scalability 瓶頸(存取 atomic 通常遠慢於非 atomic),也提醒該變數是否漏了宣告成 std::atomic。但這只是風格議題,與 atomic vs volatile 的抉擇本質不同。

Exam/Test Patterns

情境關鍵字 答案
多執行緒共享計數器、不想用 mutex std::atomicvolatile 有 data race → UB
兩執行緒各 ++ 一次 volatile int,結果可能是? 可能是 1(RMW 交錯);且屬 data race,實為 UB
兩執行緒各 ++ 一次 std::atomic<int>,結果? 必定是 2(RMW 原子)
「先算值、再設 flag」怎麼保證順序對其他執行緒可見? flag 用 std::atomic<bool>(預設 sequential consistency 禁止寫入前程式碼重排到其後);volatile 無此保證
memory-mapped I/O、感測器、控制埠 volatile:redundant loads / dead stores 不可優化
auto y = x;,x 是 volatile int,y 型別? int(auto 推導丟棄 volatile)
std::atomic<int> x; auto y = x; 能編譯嗎? 不能,copy 操作被刪除;須 std::atomic<int> y(x.load());
atomic 上的重複 load 可被編譯器優化掉嗎? 可以(可存暫存器)→ 因此 atomic 不適合特殊記憶體
既是 I/O 位址又被多執行緒存取 volatile std::atomic<T> 合用
Java/C# 的 volatile 等同 C++ 的? 較接近 std::atomic;C++ 的 volatile 與並行無關