原子型別與操作 (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>。語言定義中只有這些型別的操作是原子的;實作上可能用原子指令,也可能在內部用鎖模擬。
is_lock_free()成員函式:回傳true表示直接用原子指令;false表示內部用了鎖(此時不會比 mutex 快)。- C++17 起每個原子型別有
static constexpr成員is_always_lock_free:該型別在此平台上永遠無鎖才為true(如std::atomic<int>::is_always_lock_free)。 - 巨集
ATOMIC_BOOL_LOCK_FREE、ATOMIC_INT_LOCK_FREE、ATOMIC_POINTER_LOCK_FREE等:0 = 非無鎖、2 = 永遠無鎖、1 = 執行期才能確定。 - 只有
std::atomic_flag不提供is_lock_free()——因為它是唯一被標準保證無鎖的型別。 - 其餘型別皆為
std::atomic<>特化;歷史備選名(atomic_bool↔std::atomic<bool>、atomic_int↔std::atomic<int>…)與atomic_size_t等 typedef 也存在,不建議與std::atomic<T>寫法混用(損害可移植性)。
拷貝構造/拷貝賦值牽涉兩個物件、兩次操作(先讀一個、再寫另一個),合併必然不是原子的,因此一律被禁止。但可與對應的非原子內建型別互相賦值/隱式轉換。
每種操作都可帶一個記憶體順序 (memory ordering) 參數,依操作類別限制如下(預設一律 memory_order_seq_cst):
| 操作類別 | 代表操作 | 允許的 memory_order |
|---|---|---|
| Store | store()、clear() |
relaxed、release、seq_cst |
| Load | load() |
relaxed、consume、acquire、seq_cst |
| Read-Modify-Write (讀-改-寫) | exchange()、CAS、fetch_xxx()、test_and_set() |
全部六種 |
std::atomic_flag
最簡單的原子型別:一個可在「設置/清除」兩狀態間切換的布林旗標,只作為建構元件使用。
- 必須用
ATOMIC_FLAG_INIT初始化,初始狀態一律為「清除」;靜態儲存期物件保證靜態初始化(無初始化順序問題)。 - 僅三種操作:銷毀、
clear()(store 操作,不能用 acquire/acq_rel 語義)、test_and_set()(讀-改-寫:設置旗標並回傳舊值,可用任何記憶體順序)。 - 沒有非修改查詢(不能單純讀值),連普通布林旗標都當不了 → 實務上多改用
std::atomic<bool>。
有限的特性使它非常適合實作自旋鎖 (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);
- CAS 可指定兩個記憶體順序:成功序與失敗序。失敗不發生存儲,故失敗序不能是
release/acq_rel;(書中規則)失敗序也不能比成功序更嚴格。 - 只給一個順序時,失敗序由成功序推導:
release → relaxed、acq_rel → acquire,其餘與成功序相同;都不給則兩者皆seq_cst。
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 形式可指定記憶體順序
fetch_add()/fetch_sub()是原子讀-改-寫操作(「交換-相加」),回傳舊值;+=、-=、++、--回傳新值。- 回傳的是普通
T*值(而非原子物件的引用)。 - 運算子形式無法指定記憶體順序,一律為
memory_order_seq_cst語義;要指定順序須用fetch_xxx()。
整數原子型別的操作
std::atomic<int>、std::atomic<unsigned long long> 等,在通用操作之外提供:
fetch_add()、fetch_sub()、fetch_and()、fetch_or()、fetch_xor()(回傳舊值)- 複合賦值
+=、-=、&=、|=、^=(回傳新值),以及++x/x++/--x/x--(語義同一般整數:前綴回新值、後綴回舊值) - 缺:乘法、除法、位移——原子整數多當計數器或位元遮罩用,不需要;真需要時可用
compare_exchange_weak()迴圈完成。
自訂型別的 std::atomic<UDT>
std::atomic<> 是泛型模板,可用自訂型別 UDT 特化,但條件嚴格:
- 必須有編譯器產生的拷貝賦值運算子(trivially copyable):不能有虛函式、虛基類;所有基類與非靜態資料成員也須滿足 → 允許實作用
memcpy直接拷貝,無使用者程式碼介入。 - 比較-交換用
memcmp位元逐位比較,而不是使用者定義的operator==。 - 介面僅限:
load()、store()、exchange()、compare_exchange_weak/strong()、賦值、轉型回 T。 - 大小 ≤
int/void*的 UDT 在多數平台可用原子指令;兩倍大小者在支援 DWCAS(double-word-compare-and-swap)的平台上也可無鎖(對第 7 章無鎖程式設計很重要)。 - 不能做
std::atomic<std::vector<int>>(非 trivially copyable);含計數器、旗標、指標、簡單陣列的型別可以。複雜資料結構請改用std::mutex(見 03-Sharing-Data/01-Race-Conditions-and-Mutexes)。
- 若 UDT 有不參與比較的填充位 (padding),兩個「語義相等」的物件可能位元不同 →
compare_exchange對相等值失敗。 std::atomic<float>/std::atomic<double>合法,但相同數值可能有不同位元表示,compare_exchange_strong也可能對「相等」的值失敗——這不是浮點原子運算。- UDT 的
operator==語義若與 memcmp 不同,同樣會出現「值相等卻交換失敗」。
呼應第 3 章的建議:不要把受保護資料的指標/引用傳給使用者提供的函式。若允許使用者的拷貝賦值或比較運算子,實作就得把內部資料交給使用者程式碼,可能造成死結;限制成「一組原始位元組」讓編譯器能直接產生原子指令。
非成員函式與 shared_ptr 原子操作
- 每個成員函式都有對應的非成員版本:加
atomic_前綴,如std::atomic_load(&a)≡a.load();第一個參數是指向原子物件的指標(為了 C 語言相容,C 沒有引用)。 - 指定記憶體順序用
_explicit後綴:std::atomic_store_explicit(&a, v, std::memory_order_release)。 - CAS 非成員版的
expected參數也是指標;_explicit版需同時給成功/失敗兩個順序。 std::atomic_flag的版本命名帶 flag:std::atomic_flag_test_and_setatomic_flag_clear()(以及_explicit版。
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>()); // 原子寫
- 並行技術規範 (TS) 另提供
std::experimental::atomic_shared_ptr<T>(<experimental/atomic>):獨立型別、可有無鎖實作、不增加普通shared_ptr的開銷,且型別即保證所有存取都是原子的,避免忘記使用原子函式而產生資料競爭 → 較推薦。 - (補充)C++20 以
std::atomic<std::shared_ptr<T>>取代並棄用上述自由函式。
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> |
Related Notes
- 05-Memory-Model-and-Atomics/01-Memory-Model-Basics
- 05-Memory-Model-and-Atomics/03-Synchronizes-With-and-Happens-Before
- 05-Memory-Model-and-Atomics/04-Memory-Ordering-Options
- 05-Memory-Model-and-Atomics/05-Release-Sequences-and-Fences
- 05-Memory-Model-and-Atomics/Practice-Memory-Model-and-Atomics
- 07-Lock-Free-Data-Structures/01-Lock-Free-Concepts-and-Tradeoffs
- 03-Sharing-Data/01-Race-Conditions-and-Mutexes