名稱空間
變體
操作

memory_order

來自 cppreference.com
< c‎ | atomic
在標頭檔案 <stdatomic.h> 中定義
列舉 memory_order

{
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst

};
(C11 起)

memory_order 指定記憶體訪問(包括常規的非原子記憶體訪問)如何圍繞原子操作進行排序。在多核系統上,如果沒有約束,當多個執行緒同時讀寫多個變數時,一個執行緒觀察到的值變化順序可能與另一個執行緒寫入的順序不同。事實上,即使在多個讀取執行緒之間,變化的表觀順序也可能不同。由於記憶體模型允許的編譯器轉換,即使在單處理器系統上也會發生一些類似的效果。

語言和庫中所有原子操作的預設行為提供了順序一致的排序(見下文討論)。這種預設行為可能會損害效能,但庫的原子操作可以給定一個額外的memory_order引數來指定編譯器和處理器必須為該操作強制執行的精確約束,除了原子性之外。

目錄

[編輯] 常量

在標頭檔案 <stdatomic.h> 中定義
解釋
memory_order_relaxed 寬鬆操作:不施加任何同步或排序約束於其他讀寫操作,僅保證此操作的原子性(參見下面的寬鬆排序)。
memory_order_consume
(C++26 中已棄用)
使用此記憶體順序的載入操作在受影響的記憶體位置執行消費操作:當前執行緒中依賴於當前載入的值的讀寫不能在此載入之前重新排序。在其他執行緒中釋放相同原子變數的資料相關變數的寫入在當前執行緒中可見。在大多數平臺上,這僅影響編譯器最佳化(參見下面的釋放-消費排序)。
memory_order_acquire 使用此記憶體順序的載入操作在受影響的記憶體位置執行獲取操作:當前執行緒中的讀寫不能在此載入之前重新排序。其他執行緒中釋放相同原子變數的所有寫入在當前執行緒中可見(參見下面的釋放-獲取排序)。
memory_order_release 使用此記憶體順序的儲存操作執行釋放操作:當前執行緒中的讀寫不能在此儲存之後重新排序。當前執行緒中的所有寫入在獲取相同原子變數的其他執行緒中可見(參見下面的釋放-獲取排序),並且攜帶依賴到原子變數的寫入在消費相同原子的其他執行緒中可見(參見下面的釋放-消費排序)。
memory_order_acq_rel 使用此記憶體順序的讀-修改-寫操作既是獲取操作又是釋放操作。當前執行緒中的記憶體讀寫不能在載入之前重新排序,也不能在儲存之後重新排序。在修改之前,其他執行緒中釋放相同原子變數的所有寫入可見,並且修改在獲取相同原子變數的其他執行緒中可見。
memory_order_seq_cst 使用此記憶體順序的載入操作執行獲取操作,儲存操作執行釋放操作,讀-修改-寫操作既執行獲取操作又執行釋放操作,此外還存在一個單一的總序,所有執行緒都以相同的順序觀察所有修改(參見下面的順序一致排序)。

[編輯] 寬鬆排序

標記為 memory_order_relaxed 的原子操作不是同步操作;它們不強制併發記憶體訪問之間的順序。它們僅保證原子性和修改順序一致性。

例如,在 xy 最初為零的情況下,

// 執行緒 1:
r1 = atomic_load_explicit(y, memory_order_relaxed); // A
atomic_store_explicit(x, r1, memory_order_relaxed); // B
// 執行緒 2:
r2 = atomic_load_explicit(x, memory_order_relaxed); // C
atomic_store_explicit(y, 42, memory_order_relaxed); // D
允許產生 r1 == r2 == 42,因為儘管 A 線上程 1 中先於 B,C 線上程 2 中先於 D,但沒有任何東西能阻止 D 在 y 的修改順序中出現在 A 之前,B 在 x 的修改順序中出現在 C 之前。D 對 y 的副作用可能對執行緒 1 中的載入 A 可見,而 B 對 x 的副作用可能對執行緒 2 中的載入 C 可見。特別地,如果執行緒 2 中的 D 在 C 之前完成,這可能發生,無論是由於編譯器重新排序還是在執行時。

寬鬆記憶體排序的典型用途是遞增計數器,例如引用計數器,因為這隻需要原子性,而不需要排序或同步。

[編輯] 釋放-消費排序

如果執行緒 A 中的原子儲存被標記為 memory_order_release,並且執行緒 B 從相同變數的原子載入被標記為 memory_order_consume,並且執行緒 B 中的載入讀取了執行緒 A 中儲存的值,那麼執行緒 A 中的儲存是依賴有序先於執行緒 B 中的載入。

所有線上程 A 看來先於原子儲存發生的記憶體寫入(非原子和寬鬆原子)都成為執行緒 B 中那些載入操作攜帶依賴的操作中的可見副作用,也就是說,一旦原子載入完成,執行緒 B 中使用從載入中獲取的值的運算子和函式保證能夠看到執行緒 A 寫入記憶體的內容。

同步只在釋放消費相同原子變數的執行緒之間建立。其他執行緒可能會看到與同步執行緒不同或兩者都不同的記憶體訪問順序。

除了 DEC Alpha 之外,在所有主流 CPU 上,依賴排序都是自動的,此同步模式不需要發出額外的 CPU 指令,隻影響某些編譯器最佳化(例如,禁止編譯器對依賴鏈中涉及的物件執行推測性載入)。

此排序的典型用例包括對很少寫入的併發資料結構(路由表、配置、安全策略、防火牆規則等)的讀取訪問,以及透過指標介導的釋出器-訂閱者情況,即生產者透過指標釋出消費者可以訪問的資訊:不需要使生產者寫入記憶體的所有其他內容對消費者可見(這在弱有序架構上可能是一個昂貴的操作)。這種場景的一個例子是 rcu_dereference

請注意,目前(2015年2月)沒有已知的生產編譯器跟蹤依賴鏈:消費操作被提升為獲取操作。

[編輯] 釋放序列

如果某個原子被儲存-釋放,並且其他幾個執行緒對該原子執行讀-修改-寫操作,則形成一個“釋放序列”:所有對相同原子執行讀-修改-寫的執行緒都與第一個執行緒和彼此同步,即使它們沒有 memory_order_release 語義。這使得單個生產者-多個消費者的情況成為可能,而無需在各個消費者執行緒之間施加不必要的同步。

[編輯] 釋放-獲取排序

如果執行緒 A 中的原子儲存被標記為 memory_order_release,並且執行緒 B 從相同變數的原子載入被標記為 memory_order_acquire,並且執行緒 B 中的載入讀取了執行緒 A 中儲存的值,那麼執行緒 A 中的儲存同步於執行緒 B 中的載入。

所有線上程 A 看來先於原子儲存發生的記憶體寫入(包括非原子和寬鬆原子)都成為執行緒 B 中的可見副作用。也就是說,一旦原子載入完成,執行緒 B 保證能看到執行緒 A 寫入記憶體的所有內容。這個承諾僅在 B 實際返回 A 儲存的值或釋放序列中稍後的值時有效。

同步只在釋放獲取相同原子變數的執行緒之間建立。其他執行緒可能會看到與同步執行緒不同或兩者都不同的記憶體訪問順序。

在強序系統(x86、SPARC TSO、IBM 大型機等)上,對於大多數操作,釋放-獲取排序是自動的。此同步模式不需要發出額外的 CPU 指令;僅影響某些編譯器最佳化(例如,禁止編譯器將非原子儲存移過原子儲存-釋放,或將非原子載入提前於原子載入-獲取)。在弱序系統(ARM、Itanium、PowerPC)上,使用特殊的 CPU 載入或記憶體屏障指令。

互斥鎖,例如 互斥量原子自旋鎖,是釋放-獲取同步的一個例子:當執行緒 A 釋放鎖並由執行緒 B 獲取時,執行緒 A 上下文在臨界區(釋放之前)發生的一切都必須對執行相同臨界區的執行緒 B(獲取之後)可見。

[編輯] 順序一致排序

標記為 memory_order_seq_cst 的原子操作不僅以與釋放/獲取排序相同的方式對記憶體進行排序(在一個執行緒中先於儲存發生的一切都成為載入執行緒中的可見副作用),而且還建立了所有被如此標記的原子操作的單一總修改順序

形式上,

每個從原子變數 M 載入的 memory_order_seq_cst 操作 B,觀察以下之一:

  • 在單一總序中,M 的最後一次修改操作 A 出現在 B 之前的結果;
  • 或者,如果存在這樣的 A,B 可能觀察到 M 的某個非 memory_order_seq_cst 且不先於 A 發生的修改結果;
  • 或者,如果不存在這樣的 A,B 可能觀察到 M 的某個不相關的非 memory_order_seq_cst 修改結果。

如果有一個 memory_order_seq_cst atomic_thread_fence 操作 X 先於 B,那麼 B 觀察以下之一:

  • 在單一總序中,M 的最後一次 memory_order_seq_cst 修改出現在 X 之前的結果;
  • M 的某些不相關的修改,這些修改在 M 的修改順序中出現較晚。

對於原子變數 M 上的兩個原子操作 A 和 B,其中 A 寫入而 B 讀取 M 的值,如果存在兩個 memory_order_seq_cst atomic_thread_fence X 和 Y,並且如果 A 先於 X,Y 先於 B,且 X 在單一總序中出現在 Y 之前,那麼 B 觀察以下之一:

  • A 的效果;
  • M 的某些不相關的修改,這些修改在 M 的修改順序中出現在 A 之後。

對於 M 的兩個原子修改 A 和 B,如果滿足以下條件之一,則 B 在 M 的修改順序中出現在 A 之後:

  • 存在一個 memory_order_seq_cst atomic_thread_fence X,使得 A 先於 X,並且 X 在單一總序中出現在 B 之前;
  • 或者,存在一個 memory_order_seq_cst atomic_thread_fence Y,使得 Y 先於 B,並且 A 在單一總序中出現在 Y 之前;
  • 或者,存在 memory_order_seq_cst atomic_thread_fence X 和 Y,使得 A 先於 X,Y 先於 B,並且 X 在單一總序中出現在 Y 之前。

請注意,這意味著

1) 一旦未標記為 memory_order_seq_cst 的原子操作進入圖片,順序一致性就會丟失,
2) 順序一致的屏障只為屏障本身建立總序,而不是在一般情況下為原子操作建立總序(先於不是跨執行緒關係,不像happens-before)。

對於多生產者-多消費者的情況,如果所有消費者都必須以相同的順序觀察所有生產者的操作,則可能需要順序排序。

在所有多核系統上,總順序排序需要一個完整的記憶體屏障 CPU 指令。這可能成為效能瓶頸,因為它強制受影響的記憶體訪問傳播到每個核心。

[編輯] 與 volatile 的關係

在執行執行緒中,透過 volatile 左值進行的訪問(讀寫)不能重新排序到與同一執行緒內序列點分隔的可觀察副作用(包括其他 volatile 訪問)之後,但此順序不保證被另一個執行緒觀察到,因為 volatile 訪問不建立執行緒間同步。

此外,volatile 訪問不是原子的(併發讀寫是資料競爭),並且不進行記憶體排序(非 volatile 記憶體訪問可以在 volatile 訪問周圍自由重新排序)。

一個值得注意的例外是 Visual Studio,在預設設定下,每次 volatile 寫入都具有釋放語義,每次 volatile 讀取都具有獲取語義(Microsoft Docs),因此 volatile 可用於執行緒間同步。標準 volatile 語義不適用於多執行緒程式設計,儘管當應用於 sig_atomic_t 變數時,它們足以用於例如與在同一執行緒中執行的 訊號 處理程式進行通訊。

[編輯] 示例

[編輯] 參考文獻

  • C23 標準 (ISO/IEC 9899:2024)
  • 7.17.1/4 memory_order (p: 待定)
  • 7.17.3 順序和一致性 (p: 待定)
  • C17 標準 (ISO/IEC 9899:2018)
  • 7.17.1/4 memory_order (p: 200)
  • 7.17.3 順序和一致性 (p: 201-203)
  • C11 標準 (ISO/IEC 9899:2011)
  • 7.17.1/4 memory_order (p: 273)
  • 7.17.3 順序和一致性 (p: 275-277)

[編輯] 另見

C++ 文件,關於 memory order

[編輯] 外部連結

1.  MOESI 協議
2.  x86-TSO: x86 多處理器的一種嚴謹且實用的程式設計師模型 P. Sewell 等人,2010
3.  ARM 和 POWER 寬鬆記憶體模型入門教程 P. Sewell 等人,2012
4.  MESIF: 適用於點對點互連的兩跳快取一致性協議 J.R. Goodman, H.H.J. Hum, 2009
5.  記憶體模型 Russ Cox, 2021