記憶體順序選項 (Memory Ordering Options)
Overview Table
| 概念 | 重點 |
|---|---|
| 六種選項 | relaxed、consume、acquire、release、acq_rel、seq_cst;預設 seq_cst |
| 三種模型 | 順序一致(seq_cst)、獲取-釋放(consume/acquire/release/acq_rel)、自由(relaxed) |
| 順序一致序 | 所有 seq_cst 操作形成單一全局總順序;最直觀、成本最高 |
| 自由序 | 只保證單一變數的修改順序 + 同執行緒序前;跨變數可任意重排 |
| 獲取-釋放序 | release store 與讀到其值的 acquire load 成對同步;無全局順序 |
memory_order_consume |
只同步資料依賴鏈;C++17 不建議使用 → 一律改用 acquire |
六種選項、三種模型
每個原子操作都可帶一個記憶體順序參數(依操作類別限制,見 05-Memory-Model-and-Atomics/02-Atomic-Types-and-Operations),六個選項實際上只代表三種記憶體模型:
| 模型 | 使用的 memory_order | 同步保證 | 相對成本 |
|---|---|---|---|
| 順序一致 (sequentially consistent) | seq_cst(預設) |
所有 seq_cst 操作有全局總順序,全體執行緒認可 | 最高:多處理器需昂貴的全局同步 |
| 獲取-釋放 (acquire-release) | consume、acquire、release、acq_rel |
release↔acquire 成對 (pairwise) 同步 | 中:x86/x86-64 上幾乎不需額外指令 |
| 自由 (relaxed) | relaxed |
無跨執行緒同步;僅單一變數修改順序 | 最低 |
x86/x86-64 上獲取-釋放序不需要額外指令,順序一致序也只有 store 需要額外開銷;弱序架構(ARM、POWER 等)上差距更明顯。是否值得用較弱的順序換效能,需查閱目標處理器文件並實測。
順序一致序 (seq_cst):全局總順序
程式行為從任何角度看都保持一致順序:若所有操作都是 seq_cst,多執行緒就像在單一執行緒上以某種特定交錯執行,所有執行緒看到相同的操作順序,操作不能重排。
// 代碼 5.4:x、y 初始為 false,四個執行緒
void write_x() { x.store(true, std::memory_order_seq_cst); } // ①
void write_y() { y.store(true, std::memory_order_seq_cst); } // ②
void read_x_then_y(){ whilememory_order_seq_cst);
ifmemory_order_seq_cst) ++z; } // ③
void read_y_then_x(){ whilememory_order_seq_cst);
ifmemory_order_seq_cst) ++z; } // ④
// 四執行緒 join 後:assert(z.load()!=0) 永不觸發
- 若 ③ 讀到
y==false,代表全局總順序中「store x 發生在 store y 之前」→ ④ 等到y==true之後再讀 x,必定讀到true→++z。 - 對稱情形同理;兩個讀執行緒都讀到
true時z==2。所以 z 不可能為 0。
順序一致要求所有處理器對整個操作序列達成共識,多核機器上需昂貴的跨處理器同步。這是它「最容易推理、也最慢」的原因;不需要全局順序時,考慮獲取-釋放序。
自由序 (relaxed):只保證單一變數的修改順序
原子操作標記 relaxed 時,不建立任何跨執行緒同步。僅存的保證:
- 同一執行緒內、同一變數上的操作仍遵守序前關係與修改順序:一旦看到某值,之後的讀取不會「倒退」讀回更舊的值。
- 每個變數各自有一條全體認可的修改順序(見 05-Memory-Model-and-Atomics/01-Memory-Model-Basics),但不同變數之間完全沒有順序約束。
// 代碼 5.5:assert 可能觸發!
void write_x_then_y() {
x.store(true, std::memory_order_relaxed); // ①
y.store(true, std::memory_order_relaxed); // ②
}
void read_y_then_x() {
whilememory_order_relaxed); // ③ 讀到 true
ifmemory_order_relaxed) ++z; // ④ 仍可能讀到 false!
}
雖然 ① 序前於 ②,但 store/load 之間不成對同步,④ 可以看到「違反」寫入順序的結果 → assert(z!=0) 可能觸發。
計數員比喻(書中「小隔間裡拿記事本的人」):每個原子變數 = 一個獨立房間裡的記錄員,記事本上是該變數的值列表(修改順序)。
- 向他要值:他可以回覆「你上次拿到的值或列表更下方的任意值」,絕不會給更上方(更舊)的值;
- 請他寫值:附加到列表最後;
- 不同調用者(執行緒)問同一個記錄員,得到的答案進度可以不同——他不會主動把你寫的值告訴別人。
計數器範例(代碼 5.6):三個執行緒分別對原子計數器 x、y、z 做 relaxed 遞增,另外兩個執行緒 relaxed 讀取三者。結果:每個變數都從 0 單調升到 10(單一變數修改順序成立),但各執行緒看到的 (x,y,z) 組合彼此不同、進度不均,某個執行緒甚至可能完全看不到 x 或 y 的任何更新——這些輸出全部合法。
書中強烈建議:除非硬性需求(如純統計計數器,不用於同步其他資料),否則避免自由序;必須與具較強順序語義的原子操作搭配,且使用時需十二分謹慎。
獲取-釋放序 (acquire-release):成對同步
relaxed 的加強版:仍無全局總順序,但引入同步——原子載入是獲取操作 (acquire)、原子存儲是釋放操作 (release)、原子讀-改-寫可兼具兩者 (acq_rel)。釋放操作與「讀到其寫入值」的獲取操作同步發生,同步是成對的 (pairwise)。
// 代碼 5.8:assert 不會觸發
void write_x_then_y() {
x.store(true, std::memory_order_relaxed); // ①
y.store(true, std::memory_order_release); // ②
}
void read_y_then_x() {
whilememory_order_acquire); // ③ 自旋直到讀到 true
ifmemory_order_relaxed) ++z; // ④ 必為 true
}
執行緒 a: write_x_then_y 執行緒 b: read_y_then_x
------------------------ ------------------------
① x.store(true, relaxed)
│ sequenced-before
② y.store(true, release)
└──── synchronizes-with ─────→ ③ y.load(acquire) 讀到 true
│ sequenced-before
④ x.load(relaxed) 必讀到 true
傳遞:① happens-before ④ ⇒ assert(z!=0) 不觸發
同步的前提是 acquire 確實讀到 release(或其釋放序列)寫入的值——代碼 5.8 靠 while 迴圈保證這點。若任一端不配對(release store 配 relaxed load,或 relaxed store 配 acquire load),對 x 的存取就無序,斷言可能觸發。
沒有全局順序(代碼 5.7):把代碼 5.4 的 seq_cst 全改成 release/acquire,但 x、y 由不同執行緒寫入——每對 release/acquire 只同步該變數,x 與 y 之間無順序,兩個讀執行緒可以看到相反的寫入順序(「活在不同的世界」)→ z==0 變成合法結果。這是 seq_cst 與 acquire-release 的關鍵差異。
傳遞同步(代碼 5.9)
執行緒間先行可傳遞,獲取-釋放序能跨「中間執行緒」傳遞同步:
thread_1: 寫 data[0..4](relaxed) → sync1.store(true, release) ─┐
thread_2: while(!sync1.load(acquire)); ←── 同步 ──────────────┘
sync2.store(true, release) ─┐
thread_3: while(!sync2.load(acquire)); ←── 同步 ────────────────┘
讀 data[0..4]:必得 thread_1 寫入的值(斷言不觸發)
- thread_2 從未接觸 data,仍足以讓 thread_1 的寫入先行於 thread_3 的讀取。
- 可將 sync1/sync2 合併為單一變數,thread_2 改用
sync.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)迴圈。
RMW 操作的語義選擇:
| 寫法 | 效果 |
|---|---|
fetch_sub(acquire) |
不與任何東西同步——沒有 release 部分,它不是釋放操作 |
fetch_or(release) |
讀取部分不是獲取操作 → 不與之前的 store 同步 |
fetch_x(acq_rel) |
兼具獲取與釋放:可與前面的 release store、後面的 acquire load 皆同步 |
與 seq_cst 混用:seq_cst 載入的行為如 acquire、存儲如 release、RMW 如 acq_rel;relaxed 操作依舊自由,但會被已建立的同步關係約束。
鎖住互斥鎖是獲取操作、解鎖是釋放操作——這是理解一切鎖語義的鑰匙(詳見 05-Memory-Model-and-Atomics/05-Release-Sequences-and-Fences)。
memory_order_consume:資料依賴(不建議使用)
consume 屬於獲取-釋放模型,但很特殊:只對「資料依賴鏈」建立順序,並引入兩種關係:
- 攜帶依賴 (carries-a-dependency-to):單一執行緒內,操作 A 的結果作為操作 B 的運算元 → A 攜帶依賴至 B;可傳遞。
- 前序依賴 (dependency-ordered-before):release store 與讀到該值的 consume load 之間成立,再沿攜帶依賴鏈延伸。
// 代碼 5.10
std::atomic<X*> p; std::atomic<int> a;
// 寫者
X* x = new X; x->i = 42; x->s = "hello";
a.store(99, std::memory_order_relaxed); // ①
p.store(x, std::memory_order_release); // ②
// 讀者
X* x;
while(!(x = p.loadmemory_order_consume)); // ③
assert(x->i == 42); // ④ 不會觸發:依賴 p 的載入
assert(x->s == "hello"); // ⑤ 不會觸發:同上
assertmemory_order_relaxed) == 99; // ⑥ 可能觸發:不依賴 p!
x->i、x->s對p.load()攜帶依賴 → 保證看到寫者的初始化;a與 p 無依賴 → 不保證。若 ③ 改用acquire,⑥ 也不會觸發。std::kill_dependency()可顯式打破依賴鏈(複製參數並切斷依賴),允許編譯器快取與重排。- consume 參與執行緒間先行,但不參與強先行發生(見 05-Memory-Model-and-Atomics/03-Synchronizes-With-and-Happens-Before)。
C++17 明確不建議使用 consume:依賴追蹤難以高效實作,主流編譯器實際上直接把 consume 當 acquire 處理。實務規則:一律用 memory_order_acquire,也就不需要 std::kill_dependency()。本節僅為完整涵蓋六種順序而介紹。
Exam/Test Patterns
| 情境/關鍵字 | 答案 |
|---|---|
| 未指定記憶體順序時的預設 | memory_order_seq_cst |
| 六種選項對應幾種模型 | 三種:seq_cst / acquire-release(含 consume) / relaxed |
| 代碼 5.4(全 seq_cst)的 assert 會觸發嗎 | 不會;全局總順序保證 z ≥ 1 |
| 同場景改成 release/acquire(x、y 不同執行緒寫) | z==0 合法;成對同步無全局順序,兩讀者可見相反順序 |
| relaxed 雙變數旗標(代碼 5.5) | assert 可能觸發;不同變數間可重排 |
| 「relaxed 保證什麼?」 | 單一變數修改順序 + 同執行緒序前;無跨執行緒同步 |
| release store 配 relaxed load | 不同步;必須 release↔acquire 成對且讀到其值 |
fetch_sub(memory_order_acquire) 能與誰同步 |
不與任何 store 同步(缺 release 部分);要雙向就用 acq_rel |
| consume 與 acquire 的差異 | consume 只排序資料依賴鏈;acquire 排序其後所有存取 |
| 「consume 該用嗎?」 | 不該;C++17 不建議,一律改用 acquire |
| x86 上各序成本 | acquire-release 幾乎免費;seq_cst 僅 store 有額外開銷 |
Related Notes
- 05-Memory-Model-and-Atomics/01-Memory-Model-Basics
- 05-Memory-Model-and-Atomics/02-Atomic-Types-and-Operations
- 05-Memory-Model-and-Atomics/03-Synchronizes-With-and-Happens-Before
- 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/04-Lock-Free-Queue-and-Memory-Ordering