原子型別與操作 (Atomic Types and Operations)

Overview Table

概念 重點
原子操作 不可分割:任何執行緒都觀察不到「做了一半」的狀態
lock-free 查詢 is_lock_freeis_always_lock_free(C++17 編譯期、ATOMIC_xxx_LOCK_FREE 巨集
std::atomic_flag 唯一保證無鎖 (lock-free) 的型別;僅 test_and_set() / clear()
std::atomic<bool> exchange()compare_exchange_weak/strong()(weak 有偽失敗)
std::atomic<T*> fetch_add()/fetch_sub() 指標運算,回傳舊值
整數原子型別 fetch_and/or/xor、複合賦值;乘除與位移
std::atomic<UDT> 要求 trivially copyable;比較用 memcmp → padding 陷阱
非成員函式 std::atomic_load() 等、_explicit 後綴;shared_ptr 特例

標準原子型別總覽

標準原子型別定義於標頭 <atomic>。語言定義中只有這些型別的操作是原子的;實作上可能用原子指令,也可能在內部用鎖模擬。

原子型別不可拷貝、不可相互賦值

拷貝構造/拷貝賦值牽涉兩個物件、兩次操作(先讀一個、再寫另一個),合併必然不是原子的,因此一律被禁止。但可與對應的非原子內建型別互相賦值/隱式轉換。

每種操作都可帶一個記憶體順序 (memory ordering) 參數,依操作類別限制如下(預設一律 memory_order_seq_cst):

操作類別 代表操作 允許的 memory_order
Store store()clear() relaxedreleaseseq_cst
Load load() relaxedconsumeacquireseq_cst
Read-Modify-Write (讀-改-寫) exchange()、CAS、fetch_xxx()test_and_set() 全部六種

std::atomic_flag

最簡單的原子型別:一個可在「設置/清除」兩狀態間切換的布林旗標,只作為建構元件使用。

有限的特性使它非常適合實作自旋鎖 (spinlock)(代碼 5.1):

class spinlock_mutex {
  std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
  void lock()   { while memory_order_acquire); }
  void unlock() { flag.clearmemory_order_release; }
};

lock() 迴圈直到 test_and_set() 回傳 false(表示旗標原本是清除的、自己搶到鎖);unlock() 清除旗標。它滿足互斥,足以配合 std::lock_guard<> 使用。其記憶體順序原理見 05-Memory-Model-and-Atomics/05-Release-Sequences-and-Fences

std::atomic<bool>

功能較齊全的布林原子型別:可用非原子 bool 構造與賦值(賦值回傳 bool 而非引用,避免多餘的載入)。

std::atomic<bool> b(true);
bool x = b.loadmemory_order_acquire;  // 載入
b.store(true);                                // 存儲
x = b.exchange(false, std::memory_order_acq_rel); // 讀-改-寫:寫新值、回傳舊值

compare_exchange:「比較/交換」——原子程式設計的基石

比較原子變數目前值與 expected:相等→ 存入 desired、回傳 true;不相等→ 把 expected 更新為目前值、回傳 false

compare_exchange_weak() compare_exchange_strong()
偽失敗 (spurious failure) 可能:值相等仍失敗(缺少單條 CAS 指令的機器上,時序造成) 不會:回傳 false 必因值不相等(內部含迴圈)
慣用法 必須搭配迴圈 可單獨使用
適用 存值成本低、或本來就要寫迴圈 desired 計算昂貴、不想重算
bool expected = false;
while (!b.compare_exchange_weak(expected, true) && !expected);
std::atomic<bool> 不保證無鎖

std::atomic_flag 不同,實作可能內建互斥鎖,需用 is_lock_free() 檢查。「原子型別都無鎖」是常見誤解——標準只保證 std::atomic_flag

std::atomic<T*>:指標原子運算

介面同 atomic<bool>(load/store/exchange/CAS),另加指標算術:

Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo* x = p.fetch_add(2);  // p 前進 2 個元素,回傳「舊值」&some_array[0]
x = (p -= 1);             // 運算子形式回傳「新值」&some_array[1]
p.fetch_add(3, std::memory_order_release); // fetch 形式可指定記憶體順序

整數原子型別的操作

std::atomic<int>std::atomic<unsigned long long> 等,在通用操作之外提供:

自訂型別的 std::atomic<UDT>

std::atomic<> 是泛型模板,可用自訂型別 UDT 特化,但條件嚴格:

padding 與浮點數的 memcmp 陷阱

  • 若 UDT 有不參與比較的填充位 (padding),兩個「語義相等」的物件可能位元不同 → compare_exchange 對相等值失敗
  • std::atomic<float>/std::atomic<double> 合法,但相同數值可能有不同位元表示,compare_exchange_strong 也可能對「相等」的值失敗——這不是浮點原子運算。
  • UDT 的 operator== 語義若與 memcmp 不同,同樣會出現「值相等卻交換失敗」。

為什麼限制這麼嚴?

呼應第 3 章的建議:不要把受保護資料的指標/引用傳給使用者提供的函式。若允許使用者的拷貝賦值或比較運算子,實作就得把內部資料交給使用者程式碼,可能造成死結;限制成「一組原始位元組」讓編譯器能直接產生原子指令。

非成員函式與 shared_ptr 原子操作

std::shared_ptr<>唯一的例外——它不是原子型別,卻有自由函式形式的原子操作(load/store/exchange/CAS),打破「只有原子型別才有原子操作」的原則:

std::shared_ptr<my_data> p;
std::shared_ptr<my_data> local = std::atomic_load(&p); // 原子讀
std::atomic_store(&p, std::make_shared<my_data>());    // 原子寫

Exam/Test Patterns

情境/關鍵字 答案
「哪個原子型別保證 lock-free?」 只有 std::atomic_flag(它連 is_lock_free() 都沒有)
is_lock_free() 回傳 false 內部以模擬,無效能優勢
編譯期判斷是否永遠無鎖 C++17 X::is_always_lock_free;巨集值 2/1/0
CAS「值明明相等卻回傳 false」 compare_exchange_weak偽失敗 → 搭配迴圈;或 memcmp 遇上 padding/浮點表示差異
weak vs strong 選擇 本來就要迴圈/存值便宜 → weak;desired 計算昂貴 → strong
exchange() 語義 原子地寫入新值並回傳舊值(RMW)
fetch_add() vs += 回傳值 fetch 系回舊值;複合賦值/運算子回新值
store() 能用 memory_order_acquire 嗎?」 不能;store 僅 relaxed/release/seq_cst,load 僅 relaxed/consume/acquire/seq_cst
atomic<UDT> 的條件 trivially copyable(編譯器拷貝賦值、無虛函式/虛基類);比較用 memcmp
原子型別可以拷貝構造嗎 不可;涉及兩個物件的兩次操作,非原子
shared_ptr 要原子存取 自由函式 std::atomic_load/store(&p)experimental::atomic_shared_ptrshared_ptr>
Connected Pages
原子型別與操作 (Atomic Types and Operations)
  • Overview Table
  • 標準原子型別總覽
  • std::atomic_flag
  • std::atomic
    1. compare_exchange:「比較/交換」——原子程式設計的基石
  • std::atomic:指標原子運算
  • 整數原子型別的操作
  • 自訂型別的 std::atomic
  • 非成員函式與 shared_ptr 原子操作
  • Exam/Test Patterns
  • Related Notes