std::memory_order
定义在头文件 <atomic>
typedef enum memory_order {
memory_order_releaxed,
memory_order_consume,
memory_order_acquire, // (since C++11)
memory_order_release, // (until C++20)
memory_order_acq_rel,
memory_order_seq_cst
} memory_order;
enum class memory_order : /*unspecified*/ {
relaxed, consum, acquire, release, acq_rel, seq_cst
}
inline constexpr memory_order memory_order_relaxed = memory_order::relaxed;
inline constexpr memory_order memory_order_consume = memory_order::consume;
inline constexpr memory_order memory_order_acquire = memory_order::acquire;
inline constexpr memory_order memory_order_release = memory_order::release; // (since C++20)
inline constexpr memory_order memory_order_acq_rel = memory_order::acq_rel;
inline constexpr memory_order memory_order_seq_cst = memory_order::seq_cst;
std::memory_order 指定原子操作周围的内存访问如何进行排序,包括常规的,非原子的内存访问。当多线程同时访问几个变量时,在多核系统上缺乏任何约束,一条线程观察值的变换的顺序可能不同于另一线程的写值顺序。实际上,值改变的顺序甚至在多个读取线程中都不尽相同。一些类似的现象可能出现在单处理器系统上,因为内存模型允许编译器进行转换(读写内存顺序)。
库中提供的所有原子操作的默认行为都使用 sequentially consistent ordering(见下述讨论)。默认值可能会影响性能,但是库的原子操作能够提供一个额外的 std::memory_order 参数来指定原子性之外的具体约束,编译器和处理器必须以指定 std::memory_order 约束强制执行该操作。
Constants
定义于头文件 <atomic> 中。
memory_order_relaxed
relaxed operation:对其他读写行为无同步或顺序约束,仅保证本操作的原子性。
Relaxed ordering
标记为 memory_order_relaxed
的 Atomic 操作并非同步操作;对并行的内存访问并不强加顺序。仅保证原子性和修改顺序的一致性。例如,x 和 y 初始化为 0。
// 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 之前,在对 y 的修改的顺序中,无法避免 D 在 A 之前发生,在对 x 的修改的顺序中,无法避免 B 在 C 之前发生。D在y上的边际效应对线程1中的 load A 可见,与此同时,B在x上的边际效应对线程2中的 load C 亦可见。特别是,如果线程 2 中的 D 在 C 之前完成,这或是由于编译器的重新排序亦或时运行期间的重新排序使 D 在 C 之前完成,这种情况就有可能会发生。
线程中的 D 在 C 之前完成,对于使用 Relaxed Ordering 内存模型的原子操作,编译器可能会做一些优化,对于原子操作的执行顺序不做保证,虽然在代码形式上是顺序执行的,但是编译/运行期间的机器指令并不一定保证顺序进行。
即使使用 relaxed 内存模型,out-of-thin-air 的值也不允许循环地依赖它们自己的计算,例如,x 和 y 初始化为 0:
// Thread 1
r1 = y.load(std::memory_order_relaxed);
if(r1 == 42) x.store(r1, std::memory_order_relaxed);
// (since C++ 14)
// Thread 2
r2 = x.load(std::memory_order_relaxed);
if(r2 == 42) y.store(42, std::memory_order_relaxed);
上述代码不允许产生 r1 == r2 == 42 因为仅当 42 被存储到 x 中时,42 才有可能被存储到 y 中,它又循环的依赖 42 被存储到 y 中。注意直到 C++14,规范在技术上允许这样做,但不建议这样的实现。
Relaxed 内存模型的典型使用是逐步增加的计数器,例如 std::shared_ptr 的引用计数器,因为它仅需要原子性,不需要顺序性和同步性 (注意 shared_ptr 计数器的减少需要 acquire-release synchronization 在析构函数中)
Code
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
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';
}
Output;
Final counter value is 10000
memory_order_consume
使用此 memory order 的 load 操作,在受影响的 memory location 上执行 consume operation:当前线程中, 没有依赖当前加载的值的读或写操作能在此 load 执行之前被重新排序。在其他线程中(release 相同的 atomic variables),写 data-dependent 的值对当前线程来说是可见的。在大部分平台,该行为仅影响编译器优化(见下述 Release-Consume ordering)
Release-Consume ordering
如果线程 A 中的一个原子 store 被标记为 memory_order_release
,且线程 B 中,一个对相同变量放入原子 load 被标记为 memory_order_consume
,从线程 A 的角度,所有发生在原子 store 之前的内存写(非原子的和 relaxed 原子的)成为了线程 B 中这些操作可见的边际效应,线程 B 中 load 操作携带依赖,即,一旦原子 load 完成,线程 B 中的这些操作和函数使用从 load 中获取的值,被保证能看到线程 A 中的写内存的内容。
仅仅在 releasing 和 consuming 相同的原子变量的线程中建立同步。其他线程可能会看到与同步线程中的其一或全部线程不同的内存访问顺序。
这种顺序的典型使用案例涉及对很少写的同步数据结构的读访问(路由表,配置,安全协议,防火墙规则等),和使用指针作为中介发布的 publisher-subscribler 场景,即,当 producer 发布一个指针,通过这个指针 consumer 能够访问信息:不需要让所有 producer 写入内存的数据对 consumer 可见(在 weakly-ordered 架构上,这个操作可能是昂贵的)。这样场景的一个例子是 rcu_dereference。
Code
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
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();
}
memory_order_acquire
使用此 memory order 的 load 操作,在受影响的 memory location 上执行 acquire operation:在本次 load 之前,当前线程中任何读写操作都不能被重新排序。release 相同 atomic variable 的其他线程中的所有写操作,对当前线程都是可见的(见下述 Release-Acquire ordering)。
Release-Acquire ordering
如果线程 A 中的 atomic store 被标记为 memory_order_release
,且线程 B 中对相同变量的一个 atomic load 被标记为 memory_order_acquire,从线程 A 的角度来看,所有发生在 atomic store 之前的内存写,在线程 B 中变成 visible side-effects,即,一旦 atomic load 被完成,保证线程 A 写内存的所有内容对线程 B 可见。This promise only holds if B actually returns the value that A stored, or a value from later in the release sequence.
同步仅在多线程 releasing 和 acquiring 相同的 atomic 变量时建立。其他线程可能会看到与同步线程中的其一或全部线程不同的内存访问顺序。
在 strongly-ordered 系统上 – x86,etc-- release-acquire ordering 自动应用于大部分操作。对于这种同步模式,不会发出发出额外的 CPU 指令;只有特定的编译器优化被影响(e.g. 编译器禁止移动 non-atomic stores 到 atomic store-release 之后或在 atomic load-acquire 之前执行 non-atomic loads)。在 weakly-ordered 系统(ARM,etc),特定的 CPU load 或 memory fence 指令被使用。
互斥锁(Mutual exclusion locks),例如 std::mutex 或 atomic spinlock,是 release-acquire 同步的一个例子:当 lock 被线程 A 释放,且被线程 B 获取,发生在线程 A 上下文临界部分(在释放之前)的所有事情必须对线程 B 可见(在获取之后),线程 B 也正在执行相同的临界部分。
Code
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
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();
}
下述的例子示范了三个线程中过度的 release-acquire ordering。
#include <thread>
#include <atomic>
#include <cassert>
#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;
while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) {
expected = 1;
}
}
void thread_3()
{
while (flag.load(std::memory_order_acquire) < 2)
;
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();
}
memory_order_release
使用此 memory order 的 store 操作执行 release operation:当前线程中任何读写操作都不能在此次 store 之后重新排序。当前线程中的所有写操作对其他线程都可见,只要线程获得相同的 atomic variable(见下述 Release-Acquire ordering),且携带依赖的对原子量的写操作对其他线程可见,只要线程 consume 相同的 atomic(见下述 Release-Consume ordering)
memory_order_acq_rel
使用此 memory order 的 read-modify-write 操作既是一个 acquire operation 也是一个release operation。当前线程的任何读写在此次 store 之前和之后都不能被重新排序。release 相同原子量的其他线程中的所有写在修改(当前线程的 store)之前都是可见的,且修改(当前线程的 store)对 acquire 相同原子量的其他线程都是可见的。
memory_order_seq_cst
使用此 memory order 的 load 操作执行一个 acquire operation,store 执行 release operation,且 read-modify-write 即执行acquire operation 又执行 release operation,加上存在一个总顺序,在所有线程观察到所有操作以这个相同的顺序排序。(见下述 Sequentially-consistent ordering)
Sequentially-consistent ordering
标记为 memory_order_seq_cst 的原子操作,不仅仅是使用与 release/acquire 排序相同的方式进行内存排序(同一线程所有在 store 之前发生的操作,都对其他线程的 load 可见),也为所有这样标记的原子操作建立了一个 single total modification order 。
Formal description
线程间同步和内存排序决定 evaluations(求值)和表达式的 side effects 如何在不同执行线程间排序。他们在下述术语中被定义:
Sequenced-before
相同的线程中,evaluation A 可能排在 evaluation B 之前,如 evaluation order 所述。
Carries dependency
相同的线程中,排序在 evaluation B 之前的 evaluation A 可能也携带一个依赖到 B 中(即,B 依赖于 A),如果下述任何项成立:
- A 的值被用做 B 的操作数,除了
a) B 是对 std::kill_dependency 的调用
b) A 是内置 &&,||,?:,or,operators 的左操作数。
2)A 写入 scalar 对象 M,B 从 M 中读取
3)A 携带依赖到另一 evaluation X 中,且 X 携带依赖到 B 中。
Modification order
对特定原子量的所有修改依据向这个原子量指定的总顺序发生。
任何对原子量的修改依序发生
所有的原子操作都保证有下述四种需求:
1)Write-write coherence: 如果 evaluation A 修改 atomic M(a write),它在 evaluation B 之前发生,B 也修改 M,那么在 M 的修改顺序中 A 排在 B 之前
(对原子量的修改) A 发生在B之前,A 的执行顺序排在 B 之前。
A(W)–>B(W)
2)Read-read coherence: 如果对 atomic M 的值计算 A(a read)发生在对 M 的值计算 B 之前,且如果值 A 来自对 M 的写 X,那么 B 的值也是由 X 存储的值,或者是由 M 上的 side-effect Y 存储的值,在 M 的修改顺序中,它排在 X 之后。
X(W)–>A(\R)–>Y(W)–>B(\R)
对原子量的读顺序与发生顺序保持一致。
3)Read-write coherence: 如果 atomic M 的值计算 A 发生在 M 的 B 操作(a write)之前,那么 A 的值来自 在 M 的 modification order中 发生在 B 之前的 side-effect(a write)X。
X(W)–>A(\R)–>B(W)
4)Write-read coherence: 如果对 atomic object M 的 side effect(a write)X 发生在 M 上的操作 B(a read)之前,那么 evaluation B 将会从 X 或 在 M 的修改顺序中排在 X 之后的某一 side effect 操作 Y 中获得其值。
X(W)–>Y(W)–>B(\R)
Release sequence
在 release operation A 在一个 atomic object M 上执行之后, M 的修改顺序的最长连续子序列由以下项组成:
1)由执行 A 的相同线程执行的写 (until C++ 20)
2)任何线程对 M 的 atomic read-modify-write 操作作为 release sequence headed by A 被熟知
Dependency-ordered before
在线程中,evaluation A is dependency-ordered before evaluation B,如果下述项为真:
1)A 在某 atomic M 上执行 release operation ,且,在不同线程中,B 执行对相同的 atomic M 执行 consume operation,且 B 读取由 以 A 为首的 release sequence 任意部分所写的值。
2)A is dependency-ordered before X 且 X 携带 dependency 进入 B。
Inter-thread happens-before
在线程中,evaluation A inter-thread happens before evaluation B, 如果任何下述项为真:
- A synchronizes-with B
- A is dependency-ordered before B
- A synchronizes-with some evaluation X,and X is sequenced-before B
- A is sequenced-before some evaluation X, and X inter-thread happens before B
- A inter-thread happens-before some evaluation X, and X inter-thread happends-before B
Happens-before
抛开线程,evaluation A hanppens-before evaluation B,如果下述为真:
1)A is sequence-before B
2)A inter-thread happens before B
实现需要保证hanppen-before 关系是非循环的,通过引入额外的同步是必要的(只有在涉及消费操作才是必要的)
如果某 evaluation 修改 memory location,且其他 evaluation 读或者修改相同的 memory loaction,且如果至少一个 evaluation 不是原子操作,项目的行为是未定义的(程序有 data race)除非在两个 evaluation 中存在 happens-before 关系。
Visible side-effects
scalar M 上的 side-effect A 对 M 上的 value computation B(a read)是 visible,如果下述项全为真时:
1)A happens-before B
2)没有其他的 side effect X 使得 A happens-before X 且 X happens-before B
如果 side-effect A 对 value computation B 可见,那么对 M 的 side-effects 的最长持续子集,在修改顺序中,B 没有在 happen-before 它,被称为visible sequence of side-effects。(M 的值,由 B 决定,将成为这些 side effects 所 store 的值的其中之一)
Note:线程间同步可以归结为避免 data races(通过建立 happens before 关系)和定义在某种情况下何种 side effects 可见。
Consume operation
使用 memory_order_consume or stronger 的 atomic load 是 consume operation。注意 std::atomic_thread_fence 强加比 consume operation 更强的同步操作。
Acquire operation
使用 memory_order_acquire or stronger 的 atomic load 是 acquire operation。Mutex 的 lock() operation 也是一个 acquire operation。注意 std::atomic_thread_fence 强加比 acquire operation 更强的同步要求。
Release operation
使用 memory_order_release or stronger 的 atomic store 是 release operation。Mutex 的 unlock() 操作也是 release operation。注意 std::atomic_thread_fence 强加比 release operation 更强的同步要求。