std::memory_order
| 定義於標頭檔 <atomic> |
||
enum memory_order { |
(C++11 起) (直到 C++20) |
|
| enum class memory_order : /* 未指定 */ { |
(自 C++20 起) | |
std::memory_order 指定了記憶體存取(包括一般的、非原子的記憶體存取)如何圍繞著原子操作進行排序。在多核心系統上,若沒有施加任何約束,當多個執行緒同時讀取和寫入數個變數時,一個執行緒觀察到數值變化的順序可能不同於另一個執行緒寫入它們的順序。事實上,多個讀取執行緒之間所觀察到的變化順序甚至可能互不相同。由於記憶體模型允許編譯器進行變換,即使在單處理器系統上,也可能發生類似的效應。
該函式庫中所有原子操作的預設行為皆提供循序一致順序 (sequentially consistent ordering)(請參見下文討論)。雖然預設行為可能會損害效能,但函式庫的原子操作可以接受額外的 std::memory_order 引數,用以指定除了原子性之外,編譯器和處理器針對該操作必須強制執行的確切約束。
[編輯] 常數
| 定義於標頭檔
<atomic> | |
| 名稱 | 說明 |
memory_order_relaxed
|
寬鬆操作:不對其他讀取或寫入施加同步或排序約束,僅保證此操作的原子性(請參見下文的寬鬆順序)。 |
memory_order_consume(在 C++26 中棄用) |
具有此記憶體順序的載入操作會在受影響的記憶體位置執行消耗操作:當前執行緒中依賴於目前所載入數值的讀取或寫入,皆不能被重排到此載入操作之前。在其他執行緒中,對相同原子變數執行釋放操作的寫入,若其屬於資料依賴變數,則在當前執行緒中為可見。在大多數平台上,這僅影響編譯器最佳化(請參見下文的釋放-消耗順序)。 |
memory_order_acquire
|
具有此記憶體順序的載入操作會在受影響的記憶體位置執行取得操作:當前執行緒中的讀取或寫入皆不能被重排到此載入操作之前。其他執行緒中所有釋放相同原子變數的寫入,在當前執行緒中皆為可見(請參見下文的釋放-取得順序)。 |
memory_order_release
|
具有此記憶體順序的儲存操作執行釋放操作:當前執行緒中的讀取或寫入皆不能被重排到此儲存操作之後。當前執行緒中所有的寫入,對於取得相同原子變數的其他執行緒皆為可見(請參見下文的釋放-取得順序),且將依賴關係攜帶至該原子變數的寫入,對於消耗相同原子變數的其他執行緒亦為可見(請參見下文的釋放-消耗順序)。 |
memory_order_acq_rel
|
具有此記憶體順序的讀取-修改-寫入操作既是取得操作也是釋放操作。當前執行緒中的記憶體讀取或寫入皆不能重排到此載入之前,亦不能重排到此儲存之後。其他執行緒中所有釋放相同原子變數的寫入,在修改前皆為可見,且此修改對於取得相同原子變數的其他執行緒亦為可見。 |
memory_order_seq_cst
|
具有此記憶體順序的載入操作執行取得操作,儲存操作執行釋放操作,而讀取-修改-寫入操作既執行取得操作也執行釋放操作,此外還存在一個單一的總順序 (total order),所有執行緒皆以相同順序觀察到所有修改(請參見下文的循序一致順序)。 |
[編輯] 正式描述
執行緒間的同步與記憶體順序決定了表達式的求值 (evaluations)與副作用 (side effects)如何在不同執行執行緒之間排序。它們是根據以下術語定義的:
[編輯] 順序前於 (Sequenced-before)
在同一個執行緒中,求值 A 可能順序前於求值 B,如求值順序中所述。
攜帶依賴 (Carries dependency)在同一個執行緒中,若求值 A 順序前於求值 B,則在下列任一情況成立時,A 也可能將依賴關係攜帶至 B(即 B 依賴於 A): 1) A 的值被用作 B 的運算元,除非
a) B 是對 std::kill_dependency 的呼叫,
b) A 是內建 &&、||、?: 或 , 運算子的左運算元。
2) A 寫入純量物件 M,B 從 M 讀取。
3) A 將依賴攜帶至另一個求值 X,而 X 將依賴攜帶至 B。
|
(直到 C++26) |
[編輯] 修改順序 (Modification order)
對任何特定原子變數的所有修改都發生在該原子變數特有的總順序中。
所有原子操作皆保證符合以下四個要求:
[編輯] 釋放序列 (Release sequence)
在對原子物件 M 執行釋放操作 A 之後,M 修改順序中最長的連續子序列,其由下列各項組成:
|
1) 由執行 A 的同一個執行緒所執行的寫入。
|
(直到 C++20) |
被稱為以 A 為首的釋放序列。
[編輯] 同步於 (Synchronizes with)
若執行緒 A 中的原子儲存是釋放操作,而執行緒 B 中對相同變數的原子載入是取得操作,且執行緒 B 中的載入讀取了執行緒 A 中儲存所寫入的值,則執行緒 A 中的儲存同步於執行緒 B 中的載入。
此外,某些函式庫呼叫可能被定義為與其他執行緒上的其他函式庫呼叫同步。
依賴排序前於 (Dependency-ordered before)在執行緒之間,若下列任一情況成立,則求值 A 依賴排序前於求值 B: 1) A 在某原子 M 上執行釋放操作,且在不同的執行緒中,B 在相同的原子 M 上執行消耗操作,並且 B 讀取了由 A 所引導之釋放序列的任何部分(直到 C++20)所寫入的值。
2) A 依賴排序前於 X,且 X 將依賴攜帶至 B。
|
(直到 C++26) |
[編輯] 執行緒間發生前於 (Inter-thread happens-before)
在執行緒之間,若下列任一情況成立,則求值 A 執行緒間發生前於求值 B:
發生前於 (Happens-before)不論執行緒為何,若下列任一情況成立,則求值 A 發生前於求值 B: 1) A 順序前於 B。
2) A 執行緒間發生前於 B。
實作必須確保發生前於關係是無環的,必要時可引入額外的同步(只有在涉及消耗操作時才可能是必要的,請參見 Batty et al)。 若一個求值修改一個記憶體位置,而另一個求值讀取或修改相同的記憶體位置,且其中至少有一個求值不是原子操作,則除非這兩個求值之間存在發生前於關係,否則該程式的行為是未定義的(該程式有資料競爭)。
|
(直到 C++26) | ||
發生前於 (Happens-before)不論執行緒為何,若下列任一情況成立,則求值 A 發生前於求值 B: 1) A 順序前於 B。
2) A 同步於 B。
3) A 發生前於 X,且 X 發生前於 B。 |
(C++26 起) |
[編輯] 強發生前於 (Strongly happens-before)
不論執行緒為何,若下列任一情況成立,則求值 A 強發生前於求值 B:
|
1) A 順序前於 B。
2) A 同步於 B。
3) A 強發生前於 X,且 X 強發生前於 B。 |
(直到 C++20) | ||
|
1) A 順序前於 B。
2) A 同步於 B,且 A 與 B 皆為循序一致的原子操作。
3) A 順序前於 X,X 簡單(直到 C++26) 發生前於 Y,且 Y 順序前於 B。
4) A 強發生前於 X,且 X 強發生前於 B。
註:非正式地說,若 A 強發生前於 B,則在所有語境中 A 都表現為在 B 之前求值。
|
(自 C++20 起) |
[編輯] 可見副作用 (Visible side-effects)
若下列兩項皆成立,則純量 M 上的副作用 A(一次寫入)對於 M 上的數值計算 B(一次讀取)是可見的:
若副作用 A 對於數值計算 B 是可見的,則在修改順序中,對 M 的副作用中使得 B 不發生前於它的最長連續子集,被稱為副作用的可見序列(由 B 決定的 M 的值,將是這些副作用之一所儲存的值)。
註:執行緒間同步的核心在於防止資料競爭(透過建立「發生前於」關係)並定義哪些副作用在什麼條件下變為可見。
[編輯] 消耗操作 (Consume operation)
帶有 memory_order_consume 或更強順序的原子載入是消耗操作。請注意,std::atomic_thread_fence 施加的同步要求比消耗操作更強。
[編輯] 取得操作 (Acquire operation)
帶有 memory_order_acquire 或更強順序的原子載入是取得操作。Mutex 上的 lock() 操作也是取得操作。請注意,std::atomic_thread_fence 施加的同步要求比取得操作更強。
[編輯] 釋放操作 (Release operation)
帶有 memory_order_release 或更強順序的原子儲存是釋放操作。Mutex 上的 unlock() 操作也是釋放操作。請注意,std::atomic_thread_fence 施加的同步要求比釋放操作更強。
[編輯] 說明
[編輯] 寬鬆順序 (Relaxed ordering)
標記為 memory_order_relaxed 的原子操作不是同步操作;它們不會在並行的記憶體存取之間施加順序。它們僅保證原子性與修改順序的一致性。
例如,若 x 與 y 最初皆為零,
// Thread 1: r1 = y.load(std::memory_order_relaxed); // A x.store(r1, std::memory_order_relaxed); // B // Thread 2: r2 = x.load(std::memory_order_relaxed); // C y.store(42, std::memory_order_relaxed); // D
允許產生 r1 == r2 == 42,因為雖然在執行緒 1 中 A 順序前於 B,而在執行緒 2 中 C 順序前於 D,但沒有任何機制阻止 D 在 y 的修改順序中出現於 A 之前,且阻止 B 在 x 的修改順序中出現於 C 之前。D 對 y 的副作用可能對執行緒 1 中的載入 A 可見,而 B 對 x 的副作用可能對執行緒 2 中的載入 C 可見。具體而言,若執行緒 2 中的 D 在 C 之前完成(不論是由於編譯器重排或是在執行期發生),便可能發生這種情況。
|
即使在寬鬆記憶體模型下,「憑空產生 (out-of-thin-air)」的數值也不允許循環依賴於它們自身的計算。例如,若 x 與 y 最初皆為零, // Thread 1: r1 = y.load(std::memory_order_relaxed); if (r1 == 42) x.store(r1, std::memory_order_relaxed); // Thread 2: r2 = x.load(std::memory_order_relaxed); if (r2 == 42) y.store(42, std::memory_order_relaxed); 不允許產生 r1 == r2 == 42,因為將 42 儲存到 y 只有在儲存到 x 的值是 42 時才可能,而這又循環地依賴於儲存到 y 的值是 42。請注意,在 C++14 之前,這在技術上是被規範所允許的,但不建議實作者這麼做。 |
(C++14 起) |
寬鬆記憶體順序的典型用途是遞增計數器,例如 std::shared_ptr 的引用計數器,因為這僅要求原子性,不要求排序或同步(請注意,遞減 std::shared_ptr 計數器需要與解構函式進行取得-釋放同步)。
#include <atomic> #include <iostream> #include <thread> #include <vector> std::atomic<int> cnt = {0}; void f() { for (int n = 0; n < 1000; ++n) cnt.fetch_add(1, std::memory_order_relaxed); } int main() { std::vector<std::thread> v; for (int n = 0; n < 10; ++n) v.emplace_back(f); for (auto& t : v) t.join(); std::cout << "Final counter value is " << cnt << '\n'; }
輸出
Final counter value is 10000
[編輯] 釋放-取得順序 (Release-Acquire ordering)
若執行緒 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 載入或記憶體屏障 (fence) 指令。
互斥鎖(例如 std::mutex 或 原子自旋鎖 (atomic spinlock))是釋放-取得同步的一個例子:當鎖被執行緒 A 釋放並被執行緒 B 取得時,執行緒 A 的語境下在臨界區(釋放前)所發生的一切,對於正在執行相同臨界區的執行緒 B(取得後)皆必須可見。
#include <atomic> #include <cassert> #include <string> #include <thread> std::atomic<std::string*> ptr; int data; void producer() { std::string* p = new std::string("Hello"); data = 42; ptr.store(p, std::memory_order_release); } void consumer() { std::string* p2; while (!(p2 = ptr.load(std::memory_order_acquire))) ; assert(*p2 == "Hello"); // never fires assert(data == 42); // never fires } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); }
以下範例示範了透過釋放序列跨越三個執行緒的遞移釋放-取得順序。
#include <atomic> #include <cassert> #include <thread> #include <vector> std::vector<int> data; std::atomic<int> flag = {0}; void thread_1() { data.push_back(42); flag.store(1, std::memory_order_release); } void thread_2() { int expected = 1; // memory_order_relaxed is okay because this is an RMW, // and RMWs (with any ordering) following a release form a release sequence while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed)) { expected = 1; } } void thread_3() { while (flag.load(std::memory_order_acquire) < 2) ; // if we read the value 2 from the atomic flag, we see 42 in the vector assert(data.at(0) == 42); // will never fire } int main() { std::thread a(thread_1); std::thread b(thread_2); std::thread c(thread_3); a.join(); b.join(); c.join(); }
[編輯] 釋放-消耗順序 (Release-Consume ordering)
|
若執行緒 A 中的原子儲存被標記為 memory_order_release,執行緒 B 中對相同變數的原子載入被標記為 memory_order_consume,且執行緒 B 中的載入讀取了由執行緒 A 中儲存所寫入的值,則執行緒 A 中的儲存依賴排序前於執行緒 B 中的載入。 從執行緒 A 的視角來看,所有在原子儲存之前發生前於該儲存的記憶體寫入(非原子的與寬鬆原子的寫入),在執行緒 B 中那些載入操作將依賴攜帶至其內的操作中變為可見副作用。也就是說,一旦原子載入完成,執行緒 B 中那些使用載入所得數值的運算子與函式,保證能看到執行緒 A 寫入記憶體的內容。 同步僅在釋放與消耗同一個原子變數的執行緒之間建立。其他執行緒觀察到的記憶體存取順序可能不同於這兩個已同步執行緒中的任何一個或全部。 在除 DEC Alpha 以外的所有主流 CPU 上,依賴排序是自動建立的,不會為此同步模式發出額外的 CPU 指令,僅會影響某些編譯器最佳化(例如禁止編譯器對依賴鏈中涉及的物件執行推測性載入)。 此順序的典型使用情境包括對極少寫入的並行資料結構(路由表、組態、安全性原則、防火牆規則等)的讀取存取,以及使用指標進行發佈的發佈者-訂閱者情境。即當生產者透過一個指標發佈資訊,而消費者可以透過該指標存取資訊時:不需要讓生產者寫入記憶體的所有其他內容對消費者可見(這在弱順序架構上可能是昂貴的操作)。此情境的一個例子是 另請參見 std::kill_dependency 與 請注意,目前 (2/2015) 沒有已知的量產編譯器會追蹤依賴鏈:消耗操作會被提升為取得操作。 |
(直到 C++26) |
|
釋放-消耗順序的規範正在修訂中,暫時不建議使用 |
(自 C++17 起) (直到 C++26) |
|
釋放-消耗順序的效果與釋放-取得順序相同,且已被棄用。 |
(C++26 起) |
此範例示範了指標發佈的依賴排序同步:整數資料與字串指標之間不具有資料依賴關係,因此其值在消費者中是未定義的。
#include <atomic> #include <cassert> #include <string> #include <thread> std::atomic<std::string*> ptr; int data; void producer() { std::string* p = new std::string("Hello"); data = 42; ptr.store(p, std::memory_order_release); } void consumer() { std::string* p2; while (!(p2 = ptr.load(std::memory_order_consume))) ; assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr assert(data == 42); // may or may not fire: data does not carry dependency from ptr } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); }
[編輯] 循序一致順序 (Sequentially-consistent ordering)
標記為 memory_order_seq_cst 的原子操作不僅以與釋放/取得順序相同的方式排序記憶體(在一個執行緒中儲存之前發生前於該儲存的一切,在執行載入的執行緒中皆變為可見副作用),而且還為所有如此標記的原子操作建立一個單一的總修改順序。
| 正式地說: 每個從原子變數 M 載入的
若存在一個
對於一對 M 上的原子操作 A 與 B(A 寫入而 B 讀取 M 的值),若存在兩個
對於一對稱為 A 與 B 的對 M 的原子修改,若滿足下列條件,則 B 出現於 M 修改順序中 A 之後:
請注意,這意味著: 1) 一旦未標記為
memory_order_seq_cst 的原子操作進入畫面,循序一致性就會喪失,2) 循序一致的屏障僅為屏障本身建立總順序,在一般情況下並不為原子操作建立總順序(與發生前於不同,順序前於不是跨執行緒的關係)。 |
(直到 C++20) |
| 正式地說: 若下列任一情況成立,則原子物件 M 上的原子操作 A 一致性排序前於 (coherence-ordered-before) M 上的另一個原子操作 B: 1) A 是一個修改,且 B 讀取了由 A 儲存的值,
2) 在 M 的修改順序中 A 領先於 B,
3) A 讀取了由原子修改 X 儲存的值,X 在修改順序中領先於 B,且 A 與 B 不是同一個原子讀取-修改-寫入操作,
4) A 一致性排序前於 X,且 X 一致性排序前於 B。
在所有 1) 若 A 與 B 是
memory_order_seq_cst 操作,且 A 強發生前於 B,則 A 在 S 中領先於 B,2) 對於物件 M 上的每一對原子操作 A 與 B,其中 A 一致性排序前於 B:
a) 若 A 與 B 皆為
memory_order_seq_cst 操作,則 A 在 S 中領先於 B,b) 若 A 是
memory_order_seq_cst 操作,且 B 發生前於 某個 memory_order_seq_cst 屏障 Y,則 A 在 S 中領先於 Y,c) 若某個
memory_order_seq_cst 屏障 X 發生前於 A,且 B 是 memory_order_seq_cst 操作,則 X 在 S 中領先於 B,d) 若某個
memory_order_seq_cst 屏障 X 發生前於 A,且 B 發生前於 某個 memory_order_seq_cst 屏障 Y,則 X 在 S 中領先於 Y。正式定義確保了: 1) 單一總順序與任何原子物件的修改順序一致,
2)
memory_order_seq_cst 載入獲取的值,要麼來自最後一個 memory_order_seq_cst 修改,要麼來自某個不發生前於先前 memory_order_seq_cst 修改的非 memory_order_seq_cst 修改。單一總順序可能與「發生前於」不一致。這允許在某些 CPU 上更有效地實作 例如,若 // Thread 1: x.store(1, std::memory_order_seq_cst); // A y.store(1, std::memory_order_release); // B // Thread 2: r1 = y.fetch_add(1, std::memory_order_seq_cst); // C r2 = y.load(std::memory_order_relaxed); // D // Thread 3: y.store(3, std::memory_order_seq_cst); // E r3 = x.load(std::memory_order_seq_cst); // F 允許產生 r1 == 1 && r2 == 3 && r3 == 0,其中 A 發生前於 C,但 C 在 請注意: 1) 一旦未標記為
memory_order_seq_cst 的原子操作進入畫面,程式的循序一致性保證就會喪失,2) 在許多情況下, memory_order_seq_cst 原子操作可以相對於同一個執行緒執行的其他原子操作進行重排。 |
(自 C++20 起) |
在多個生產者-多個消費者的情況下,若所有消費者必須觀察到所有生產者的動作以相同順序發生,則可能需要循序一致順序。
全循序一致順序在所有多核心系統上都需要全記憶體屏障 (full memory fence) CPU 指令。這可能會成為效能瓶頸,因為它會強制受影響的記憶體存取傳播到每個核心。
此範例示範了需要循序一致順序的情況。任何其他順序都可能觸發斷言 (assert),因為執行緒 c 與 d 可能會以相反的順序觀察到原子變數 x 與 y 的變化。
#include <atomic> #include <cassert> #include <thread> std::atomic<bool> x = {false}; std::atomic<bool> y = {false}; std::atomic<int> z = {0}; 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() { while (!x.load(std::memory_order_seq_cst)) ; if (y.load(std::memory_order_seq_cst)) ++z; } void read_y_then_x() { while (!y.load(std::memory_order_seq_cst)) ; if (x.load(std::memory_order_seq_cst)) ++z; } int main() { std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); a.join(); b.join(); c.join(); d.join(); assert(z.load() != 0); // will never happen }
[編輯] 與 volatile 的關係
在單個執行執行緒內,透過 volatile 左值 (glvalues) 的存取(讀取與寫入)不能與在同一個執行緒中順序前於或順序後於的可見副作用(包括其他 volatile 存取)重排,但不能保證其他執行緒也能觀察到此順序,因為 volatile 存取不會建立執行緒間同步。
此外,volatile 存取不是原子的(並行的讀取與寫入是資料競爭),且不會對記憶體進行排序(非 volatile 記憶體存取可以圍繞著 volatile 存取自由重排)。
一個值得注意的例外是 Visual Studio,在預設設定下,每個 volatile 寫入皆具有釋放語義,而每個 volatile 讀取皆具有取得語義(Microsoft Docs),因此 volatile 可用於執行緒間同步。標準的 volatile 語義不適用於多執行緒程式設計,儘管它們對於例如與在同一個執行緒中運行的 std::signal 處理常式進行通訊(當應用於 sig_atomic_t 變數時)是足夠的。
[編輯] 參見
| 關於 記憶體順序 的 C 文件
|
[編輯] 外部連結
| 1. | MOESI 協定 |
| 2. | x86-TSO: A Rigorous and Usable Programmer’s Model for x86 Multiprocessors P. Sewell et. al., 2010 |
| 3. | A Tutorial Introduction to the ARM and POWER Relaxed Memory Models P. Sewell et al, 2012 |
| 4. | MESIF: A Two-Hop Cache Coherency Protocol for Point-to-Point Interconnects J.R. Goodman, H.H.J. Hum, 2009 |
| 5. | Memory Models Russ Cox, 2021 |
| 本節尚不完整 原因:讓我們尋找關於 QPI、MOESI,或許還有 Dragon 的良好參考資料。 |