std::memory_order
| 在標頭 <atomic> 定義
|
||
| (C++11 起) (C++20 前) |
||
| (C++20 起) | ||
std::memory_order 指定內存訪問,包括常規的非原子內存訪問,如何圍繞原子操作排序。在沒有任何約束的多處理器系統上,多個線程同時讀或寫數個變量時,一個線程能觀測到變量值更改的順序不同於另一個線程寫它們的順序。實際上,更改的順序甚至能在多個讀取線程間相異。一些類似的效果還能在單處理器系統上出現,因為內存模型允許編譯器進行變換。
庫中所有原子操作的默認行為提供序列一致定序(見後述討論)。該默認行為可能有損性能,不過可以給予庫的原子操作額外的 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
|
有此內存定序的加載操作進行獲得操作,存儲操作進行釋放操作,而讀修改寫操作進行獲得操作和釋放操作,再加上存在一個單獨全序,其中所有線程以同一順序觀測到所有修改(見下方序列一致定序)。 |
正式描述
線程間同步和內存定序決定了表達式的求值 和副作用 如何在不同的執行線程間排序。它們用下列術語定義:
先序於
在同一線程中,求值 A 可以先序於求值 B,如求值順序中所描述。
攜帶依賴在同一線程中,若下列任一為真,則先序於求值 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 前) |
修改順序
對一個特定的原子變量的修改,以限定於此原子變量的單獨全序進行。
對所有原子操作保證下列四個要求:
釋放序列
在原子對象 M 上執行一次釋放操作 A 之後,M 的修改順序的最長連續子序列由下列內容組成:
|
1) 由執行 A 的同一線程所執行的寫操作。
|
(C++20 前) |
被稱為以 A 為首的釋放序列。
同步於
如果在線程 A 上的一個原子存儲是釋放操作,在線程 B 上的對相同變量的一個原子加載是獲得操作,且線程 B 上的加載讀取由線程 A 上的存儲寫入的值,則線程 A 上的存儲同步於線程 B 上的加載。
此外,某些庫調用也可能定義為同步於其它線程上的其它庫調用。
依賴先序於在線程間,若下列任一為真,則求值 A 依賴先序於 求值 B 1) A 在某原子對象 M 上進行釋放操作,而不同的線程中,B 在同一原子對象 M 上進行消費操作,而 B 讀取 A 所引領的釋放序列的任何部分(C++20 前)所寫入的值。
2) A 依賴先序於 X 且 X 攜帶依賴到 B。
|
(C++26 前) |
線程間先發生於
在線程間,若下列任一為真,則求值 A 線程間先發生於 求值 B
先發生於無關乎線程,若下列任一為真,則求值 A 先發生於 求值 B: 1) A 先序於 B。
2) A 線程間先發生於 B。
要求實現確保先發生於 關係是非循環的,若有必要則引入額外的同步(若引入消費操作,它才可能為必要,見 Batty 等)。 若一次求值修改一個內存位置,而其他求值讀取或修改同一內存位置,且至少一個求值不是原子操作,則程序的行為未定義(程序有數據競爭),除非這兩個求值之間存在先發生於 關係。
|
(C++26 前) | ||
先發生於不管是何線程,若下列之一為真,則求值 A 先發生於 求值 B: 1) A 先序於 B
2) A 同步於 B
3) A 先發生於 X,而 X 先發生於 B
|
(C++26 起) |
強先發生於
無關乎線程,若下列之一為真,則求值 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 起) |
可見副作用
若下列皆為真,則標量 M 上的副作用 A(寫入)相對於 M 上的值計算(讀取)可見:
如果副作用 A 相對於值計算 B 可見,那麼 M 上滿足 B 不先發生於 的副作用,在 M 上按修改順序 排列的最長連續子集,稱為副作用的可見序列(由 B 確定的 M 值將是這些副作用之一存儲的值)。
標準要求實現應當確保原子存儲操作在一個合理的時間內對其它原子加載操作可見(但並未明確「合理的時間」的要求)。
注意:線程間同步可歸結為避免數據競爭(通過建立先發生於關係),及定義在何種條件下哪些副作用成為可見。數據的可見性與 CPU 緩存強相關。
消費操作
帶 memory_order_consume 或更強標籤的原子加載是消費操作。注意 std::atomic_thread_fence 會施加比消費操作更強的同步要求。
獲得操作
帶 memory_order_acquire 或更強標籤的原子加載是獲得操作。互斥上的 lock() 操作亦為獲得操作。注意 std::atomic_thread_fence 會施加比獲得操作更強的同步要求。
釋放操作
帶 memory_order_release 或更強標籤的原子存儲是釋放操作。互斥上的 unlock() 操作亦為釋放操作。注意 std::atomic_thread_fence 會施加比釋放操作更強的同步要求。
解釋
寬鬆定序
被標以 memory_order_relaxed 的原子操作不是同步操作;它們不會為並發的內存訪問行為添加定序約束。它們只保證原子性和修改順序的一致性。
例如,對於初始值為零的 x 和 y,
// 线程 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// 线程 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D
允許產生 r1 == r2 && r2 == 42。因為即使線程 1 中 A 先序於 B 且線程 2 中 C 先序於 D,卻無法避免在 y 的修改順序中 D 會出現於 A 之前,且在 x 的修改順序中 B 會出現於 C 之前。D 的對 y 的副效應可能可見於線程 1 中 A 的加載操作,而 B 對 x 的副效應可能可見於線程 2 中 C 的加載操作。尤其是,這可能在線程 2 中 D 於 C 之前完成的情況下發生,無論因為編譯器重排還是發生於運行時。
|
即使使用寬鬆內存模型,也不允許「無中生有」的值循環地依賴於其各自的計算,例如,對於初始值為零的 // 线程1:
r1 = y.load(std::memory_order_relaxed);
if (r1 == 42)
x.store(r1, std::memory_order_relaxed);
// 线程2:
r2 = x.load(memory_order_relaxed);
if (r2 == 42)
y.store(42, std::memory_order_relaxed);
不會出現 |
(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 << "最终计数器值为 " << cnt << '\n';
}
輸出:
最终计数器值为 10000
釋放-獲取定序
若線程 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、Power PC)上,必須使用特別的 CPU 加載或內存柵欄指令。
互斥鎖(例如 std::mutex 或原子自旋鎖)是釋放-獲得同步的例子:線程 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"); // 绝无问题
assert(data == 42); // 绝无问题
}
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 是可以的,因为这是一个 RMW 操作
// 而 RMW(以任意定序)跟在释放之后将组成释放序列
while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed))
{
expected = 1;
}
}
void thread_3()
{
while (flag.load(std::memory_order_acquire) < 2)
;
// 如果我们从 atomic flag 中读到 2,将看到 vector 中储存 42
assert(data.at(0) == 42); //决不出错
}
int main()
{
std::thread a(thread_1);
std::thread b(thread_2);
std::thread c(thread_3);
a.join(); b.join(); c.join();
}
釋放-消費定序
|
若線程 A 中的原子存儲被標以 線程 A 視角中先發生於原子存儲的所有內存寫入(非原子和寬鬆原子的),會在線程 B 中該加載操作所攜帶依賴進入的操作中變成可見副效應,即一旦完成原子加載,則保證線程 B 中,使用從該加載獲得的值的運算符和函數,能見到線程 A 寫入內存的內容。 同步僅在釋放和消費同一原子變量的線程間建立。其他線程能見到與被同步線程的一者或兩者相異的內存訪問順序。 在除 DEC Alpha 之外的所有主流 CPU 上,依賴定序是自動的,無需為此同步模式發出額外的 CPU 指令,只有某些編譯器優化會受影響(例如,編譯器被禁止牽涉到依賴鏈的對象上的推測性加載)。 此定序的典型使用情況,包括對很少被寫入的並發數據結構(路由表、配置、安全策略、防火牆規則等)的讀取訪問,和有指針中介發佈的發佈者-訂閱者的情形,即生產者所發佈的指針,消費者能通過其訪問信息:無需令生產者寫入內存的所有其他內容對消費者可見(這在弱順序架構上可能是昂貴的操作)。這種場景的例子之一是 細粒度依賴鏈控制可參閱 std::kill_dependency 及 注意到 2015 年 2 月為止沒有任何已知產品級編譯器跟蹤依賴鏈:消費操作均被提升為獲得操作。 |
(C++26 前) |
|
釋放消費定序的規範正在修訂中,而且暫時不鼓勵使用 |
(C++17 起) (C++26 前) |
|
釋放消費定序的效果與釋放獲取定序相同,且已被棄用。 |
(C++26 起) |
此示例演示用於指針中介的發佈的依賴定序同步:int data 不由數據依賴關係關聯到指向字符串的指針,從而其值在消費者中未定義。
#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"); // 绝无出错: *p2 从 ptr 携带依赖
assert(data == 42); // 可能也可能不会出错: data 不从 ptr 携带依赖
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
序列一致定序
被標為 memory_order_seq_cst 的原子操作不僅以與釋放-獲得定序相同的方式進行內存定序(在一個線程中先發生於存儲的任何副作用都變成進行加載的線程中的可見副作用),還對所有帶此標籤的內存操作建立了一個單獨全序。
| 正式而言,
對原子對象 M 進行加載的每個
若存在
設有 M 上的一對原子操作,稱之為 A 和 B,這裏 A 寫入、B 讀取 M 的值,若存在二個
設有 M 上的一對原子操作,稱之為 A 和 B,若符合下列條件之一,則 M 的修改順序中 B 先發生於 A:
注意這表明: 1) 一旦出現未標記
memory_order_seq_cst 的原子操作,則立即喪失序列一致性,2) 序列一致柵欄僅為柵欄自身建立全序,而不為通常情況下的原子操作建立(先序於 不是跨線程關係,不同於先發生於) |
(C++20 前) |
| 正式而言,
某原子對象 M 上的原子操作連貫先序於 M 上的另一原子操作 B,若下列任一為真: 1) A 是修改,而 B 讀取 A 所存儲的值,
2) A 在 M 的修改順序中前於 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 操作,則 S 中 A 前於 B,b) 若 A 是
memory_order_seq_cst 操作,而 B 先發生於 memory_order_seq_cst 柵欄 Y,則 S 中 A 前於 Y,c) 若
memory_order_seq_cst 柵欄 X 先發生於 A,而 B 為 memory_order_seq_cst 操作,則 S 中 X 前於 B,d) 若
memory_order_seq_cst 柵欄 X 先發生於 A,而 B 先發生於 memory_order_seq_cst 柵欄 Y,則 S 中 X 前於 Y。正式定義確保: 1) 單獨全序與任何原子對象的修改順序一致。
2)
memory_order_seq_cst 加載到的值,要麼來自最後一次 memory_order_seq_cst 修改,要麼來自某個不先發生於順序中之前的 memory_order_seq_cst 修改操作的非 memory_order_seq_cst 修改。單獨全序可能與先發生於不一致。這允許 例如,對於初值為零的 // 线程 1 :
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B
// 线程 2 :
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed); // D
// 线程 3 :
y.store(3, std::memory_order_seq_cst); // E
r3 = x.load(std::memory_order_seq_cst); // F
允許這些操作產生 注意: 1) 一旦出現未標記
memory_order_seq_cst 的原子操作,程序的序列一致保證就會立即喪失,2) 多數情況下, memory_order_seq_cst 原子操作相對於同一線程所進行的其他原子操作可重排。 |
(C++20 起) |
在多生產者-多消費者的情形中,若所有消費者都必須以相同順序觀察到所有生產者的動作出現,則可能必須進行序列定序。
全序列定序在所有多核系統上都要求完全的內存柵欄 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); //决不发生
}
與 volatile 的關係
在執行線程中,不能將通過 volatile 泛左值進行的訪問(讀和寫)重排到同線程內先序於 或後序於 它的可觀測副效應(包含其他 volatile 訪問)後,但不保證另一線程觀察到此順序,因為 volatile 訪問不建立線程間同步。
另外,volatile 訪問不是原子的(共時的讀和寫是數據競爭),且沒有內存定序(非 volatile 內存訪問可以自由地重排到 volatile 訪問前後)。
一個值得注意的例外是 Visual Studio,其中默認設置下,每個 volatile 寫擁有釋放語義,而每個 volatile 讀擁有獲得語義(微軟文檔),故而可將 volatile 對象用於線程間同步。標準的 volatile 語義不可應用於多線程編程,儘管它們在應用到 sig_atomic_t 對象時,足以用於例如與運行於同一線程的 std::signal 處理函數間的通信。可以使用編譯器選項 /volatile:iso 恢復和標準一致的行為,當目標平台是 ARM 時這是默認設置。
參閱
memory order 的 C 文檔
|
外部連結
| 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 |
| 本節未完成 原因:讓我們在 QPI、MOESI,也許還有 Dragon 上找到好的參考資料。 |