《C++ concurrency in action》5.3 Synchronizing operations and enforcing ordering
首先就是对于程序来说都是通过 "happens-before" 和 "synchronizes-with" 来 enforcing ording
synchronizes-with
The basic idea is this: a suitably-tagged atomic write operation, W, on a variable, x, syn
chronizes with a suitably-tagged atomic read operation on x that reads the value stored
by either that write, W, or a subsequent atomic write operation on x by the same thread
that performed the initial write, W, or a sequence of atomic read-modify-write operations
on x (such as fetch_add() or compare_exchange_weak()) by any thread, where the
value read by the first thread in the sequence is the value written by W (see section 5.3.4).
主要的意思就是:一个合适标记的 atomic 写操作 W(比如用 memory_order_release)在变量 x 上,可以和一个合适标记的 atomic 读操作 R(比如用 memory_order_acquire)在 x 上建立同步关系(synchronizes with),前提是这个读操作 R 实际读取的是:
- 写操作 W 写入的值,或
- 同一线程后续的 atomic 写入值(写在 W 之后),或
- 任意线程执行的后续的 read-modify-write 操作写入的值
只有这样才会发生同步,注意 synchronizes-with 只能发生在 atomic operations
happens before
happens befor操作的理解是比较直观的,就是在同一个线程中,对于写在前面的 statements happens before 后面的 statements.
要注意的一个点是,如果是写在同一个 statement 的,那么它的顺序是 unspecified,一个例子是
#include <iostream>void foo(int a,int b){std::cout<<a<<”,”<<b<<std::endl;}int get_num(){static int i=0;return ++i;}int main(){foo(get_num(),get_num()); // calls to get_num() are unordered
}
所以最后的答案可能会输出 "1,2" 或者 "2,1"
而对于 inter-threads 不同程序之间的 happens-before 关系是通过 synchronizes-with 来维护的,并且 happens-before 有传递性,thread A happens-before thread B,并且 thread B happens-before thread C,那么 A happens-before C
理解了程序是通过 happens-before 和 synchronizes-with 来 specifies which operations see the effects of which other operations. 来学习一下对于原子操作的 memory ordering
尽管有6种 memory ordering options(memory_order_relaxed, memory_order_consume, memory_order_acquire, memory
_order_release, memory_order_acq_rel, and memory_order_seq_cst), 但是实际上只有3种 memory models:
- sequentially consistent
- acquire-release
- relaxed
Sequentially consitent
对于顺序一致性模型他的理解是很符合我们的直觉的,就是对于整个程序他会有一个total ordering,就是 all threads
must see the same order of operations.
Any sequentially con
sistent atomic operations done after that load must also appear after the store to other
threads in the system using sequentially consistent atomic operations.
就是说:如果一个线程执行了一个 seq_cst 的 atomic load,这个 load 读到了另一个线程通过 seq_cst atomic store 写入的值,那么:
- 该线程中在 load 之后发生的所有 seq_cst 原子操作,
- 在其他线程看来,也都必须发生在那个 store 之后。
这保证了所有线程对 seq_cst 原子操作的观察顺序是一致的 —— 所以叫“顺序一致性”(sequential consistency)。
举个例子就是:
std::atomic<int> x{0}, y{0};// Thread A
x.store(1, std::memory_order_seq_cst);
y.store(2, std::memory_order_seq_cst);// Thread B
int r1 = y.load(std::memory_order_seq_cst);
int r2 = x.load(std::memory_order_seq_cst);
如果 Thread B 看到 r1 == 2(说明看到的是 A 的 store(y, 2)),那么它也必须看到 x == 1,因为:
B 在 load(y) 之后又做了 load(x),而这些都是 seq_cst 的。
所以它的观察顺序中,store(x, 1) 也必须在 load(x) 之前完成。
是 default option, 但是性能影响最大。
Relaxed
对于 Relexed 的理解,在文章中使用了一个场景,将每一个 atomic 变量看成了一个人 + 笔记本,
他做两个操作:
- 记录你告诉他的新值,将其写在末尾
- 你可以从他那里得到一个值,这个值是他笔记本上的其中一个值,等他告诉你这个值之后,
后续你得到的值要么是你上一次得到的值要么是在其后面的值
更容易理解的说就是,他会记录当前对每一个人他返回的值,之后你再询问他只会回你在当前值后面得到的值或者是当前值
对 Anne 来说,她下一次可能得到的值就只可能是 2, 42 或者 67 了,对应的5,10,23...就是这个modification order.
然后如果是 relaxed 的话,有多个 atomic 变量,那就是会有很多个这样的 man,并且你询问他的值的时候,
他不一定会返回给你最新的值
一个例子是这样的:
std::atomic<bool> x,y;
std::atomic<int> z;void write_x_then_y()
{x.store(true,std::memory_order_relaxed);y.store(true,std::memory_order_relaxed);
}
void read_y_then_x()
{while(!y.load(std::memory_order_relaxed));if(x.load(std::memory_order_relaxed)) ++z;
}int main()
{x=false;y=false;z=0;std::thread a(write_x_then_y);std::thread b(read_y_then_x);a.join();b.join();cout << (z.load()!=0 ? "success" : "reordered") << endl;
}
在 relaxed 的情况下,实际上就是线程之间没有 "synchronizes-with" 关系,所以这里 z 的值是有可能为 0 的,
因为他只有相同线程当中的 happens-before 关系
于是是有可能说在 y 被观察到说 = true 的情况下,x = false,因为这就相当于对于 thread a 来说他的确通知了 x
去添加一个 true 的记录,但是 x 可能随机选择一个值,然后并没有将这个 updated 的值返回给你,这是有可能的
Acquire-release
然后就有 "Acquire-release",就是通过 release-acquire 来实现一个线程之间的 synchronizes-with 的关系,
当然按照对应 synchronizes-with 的语义一样:只有对应 read 读到的值是之前提到的3种情况的时候,才会发生同步
理解它的话还是以之前 relaxed 的场景,只是如果是 release store 的情况下,除了会告诉 man 对应要加的数字之外
还会告诉他对应的批次,而对应 acquire load 当然他也是随机给你一个值,但是这次他选择的范围除了在顺序上要>=
上一次告诉你的值之外,他选择的值还必须要是你知道的批次中的值,或者其之后的值,这就是是 release 语义的意思
here’s where the acquire
release semantics kick in: if you tell the man all the batches you know about when you
ask for a value, he’ll look down his list for the last value from any of the batches you
know about and either give you that number or one further down the list.
一个例子就是:
std::atomic<bool> x,y;
std::atomic<int> z;
void write_x_then_y()
{x.store(true,std::memory_order_relaxed); y.store(true,std::memory_order_release);
}
void read_y_then_x()
{while(!y.load(std::memory_order_acquire)); if(x.load(std::memory_order_relaxed)) ++z;
}
int main(){x=false;y=false;z=0;std::thread a(write_x_then_y);std::thread b(read_y_then_x);a.join();b.join();assert(z.load()!=0);
}
整个故事就是首先 a 运行 write_x_then_y 他告诉对应管理 x 的 man,说 “Please write true as part of batch 1 from thread a”,然后和管理 y 的说 “Please write true as the last write of batch 1 from thread a,”
同时,b 会不断的询问 y 一个值和对应的 batch 信息,知道他获得了 true,在这个过程中,他会知道对应的 batch, 也因为他知道
了对应的 batch, 当他询问的时候,他不仅仅是要一个值,他还会附上他知道的 batch 信息 "“Please can I have a value, and by the way I know about batch 1 from thread a" 于是他才可以得到 x 为 true,即使在存储 x 的时候使用的是 relaxed.
这也就是为什么说同步的前提是 read 的值是对应的3种情况的时候才能发生同步,因为只有那样才知道说对应新的 batch,也才能使用
此 batch 信息去获得对应的值
而如果这里 y 的 store 和 load 是 relaxed,就算是获得了 y = true 的情况下,因为没有 batch 信息,他依旧可能获得 x = fase,从而 z = 0