記憶體順序選項 (Memory Ordering Options)

Overview Table

概念 重點
六種選項 relaxedconsumeacquirereleaseacq_relseq_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) consumeacquirereleaseacq_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) 永不觸發
簡單就要付出代價

順序一致要求所有處理器對整個操作序列達成共識,多核機器上需昂貴的跨處理器同步。這是它「最容易推理、也最慢」的原因;不需要全局順序時,考慮獲取-釋放序。

自由序 (relaxed):只保證單一變數的修改順序

原子操作標記 relaxed 時,不建立任何跨執行緒同步。僅存的保證:

// 代碼 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 的任何更新——這些輸出全部合法。

強烈建議避免 relaxed

書中強烈建議:除非硬性需求(如純統計計數器,不用於同步其他資料),否則避免自由序;必須與具較強順序語義的原子操作搭配,且使用時需十二分謹慎。

獲取-釋放序 (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 寫入的值(斷言不觸發)

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 操作依舊自由,但會被已建立的同步關係約束。

mutex 就是 acquire-release

鎖住互斥鎖是獲取操作、解鎖是釋放操作——這是理解一切鎖語義的鑰匙(詳見 05-Memory-Model-and-Atomics/05-Release-Sequences-and-Fences)。

memory_order_consume:資料依賴(不建議使用)

consume 屬於獲取-釋放模型,但很特殊:只對「資料依賴鏈」建立順序,並引入兩種關係:

// 代碼 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!
不要在程式碼中使用 memory_order_consume

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 有額外開銷