这篇文章简述C++11之后的内存模型和Atomic中使用的内存序(memory order)。个人作品,禁止转载
参考文献
Memory Models for C/C++ Programmers
相关概念
happens-before
如果语句A和语句B在代码中依次出现,且B的运行依赖A产生的结果,则A必须在B之前执行且B能够看到A的结果,即A happens before B
// example
auto A = 3;
auto B = A * A + 123;
auto C = 10;
则程序的正确运行必须保证A happens before B,但是不需要保证A/B happens before C。
store & load
store即向内存写入,load即从内存读取
synchronized-with
举例说明:考虑2个线程
// thread A
prepare_data();
ready_flag = true; // <---------------------- point A1
do_other_things();
// thread B
while(ready_flag == false); // <----------------------- point B1
process_data();
A线程按顺序执行完point A1语句之后,B线程开始执行point B1之后的语句。这种关系是一种同步关系,即B synchronized-with A。这只是一个例子,并非synchronized-with的准确定义
内存模型与内存序
重排
再看这个例子
// thread A
prepare_data();
ready_flag = true; // <---------------------- point A1
do_other_things();
现代计算机的CPU比内存访问快太多,因此出于性能考虑,要将部分指令重排,以减少内存访问次数。比如编译器或者CPU可能将如上片段重排为
ready_flag = true; // <---------------------- point A1
prepare_data();
do_other_things();
这使得程序执行错误。因为编译器并不知道ready_flag = true;
这句话不能向上重排。因此需要对指令重排进行限制,具体体现为对内存序。
Atomic
C++的原子类型,定义了如下方法
load(std::memory_order order)
store(T desired, std::memory_order order)
exchange(T desired, std::memory_order order)
compare_exchange_weak(T& expected, T desired, std::memory_order order)
compare_exchange_strong(T& expected, T desired, std::memory_order order)
对整数类型和浮点类型,定义了自加和自减
fetch_add(T arg, std::memory_order order)
fetch_sub(T arg, std::memory_order order)
对整数类型,还定义了原子位运算
fetch_and(T arg, std::memory_order order)
fetch_or(T arg, std::memory_order order)
fetch_xor(T arg, std::memory_order order)
默认情况下,memory_order 为 memory_order_seq_cst,即顺序一致性。比如
atomic<int> i32;
i32++; // memory_order_seq_cst
i32.fetch_add(2); // memory_order_seq_cst
内存序
C++11定义了如下内存序
memory_order_seq_cst
memory_order_acq_rel
memory_order_release
memory_order_acquire
memory_order_consume
memory_order_relaxed
其中
memory_order_acquire
,memory_order_consume
只能用于读操作memory_order_release
只能用于写操作memory_order_acq_rel
只能用于读-改-写操作memory_order_relaxed
即不需要限制内存序,只需要保证原子性memory_order_seq_cst
顺序一致性,即不允许任何重排,默认
解释如下(如下解释中,load和store都是针对同一个atomic变量)
内存序 | 解释 |
---|---|
memory_order_acquire | 当前load操作之后的所有读、写操作都不能被重排到该load操作上方 |
memory_order_comsume | 当前load操作之后的,所有依赖于当前所load的变量的读、写操作都不能被重排到该load操作上方 |
memory_order_acq_rel | 本线程中,所有该操作上方的读写操作都不能被重排到该操作下方,该操作下方的读写操作也不能被重排到该操作上方。其它线程中,用release进行的store操作之前的所有写操作,都在该读写改操作之前可见 |
memory_order_release | 本线程中该操作之前的所有读写操作都不能被重排到该操作下方 |
memory_order_seq_cst | 可以用于store和load,也可以用于读写改。该操作之前的所有读写,不允许被重排到该操作下方;该操作之后的所有读写,不允许被重排到该操作上方 |
原子量线程同步的例子
如下三种内存序组合,可以用于线程同步
- memory_order_seq_cst and memory_order_seq_cst
- memory_order_release and memory_order_acquire
- memory_order_release and memory_order_consume
memory_order_seq_cst and memory_order_seq_cst
// thread A
c = 0;
x = 0;
a = 1;
b = another_variable;
x.store(1, std::memory_order_seq_cst); // <----------------Point A
c = 2;
// thread B
cout << c << endl;
while(x.load(std::memory_order_seq_cst) == 1); // <----------------Point A
cout << a << endl;
线程B的输出,只有可能是0,1。在Point A之前,A线程中a必然已经被赋值,c必然是0。Point A之前的读写操作不会跑到Point A之后,反之亦然
memory_order_release and memory_order_acquire
// thread A
c = 0;
......
b = 1;
x.store(1, std::memory_order_release); // <----------------Point A
c = 2;
// thread B
while(x.load(std::memory_order_acquire) == 1); // <----------------Point A
cout << b << c << endl;
线程B的输出,变量b一定是1,因为x.store(1, std::memory_order_release);
之前的读写操作都不能被重排到它下方,线程B的load操作之后的所有读写操作,都不会被重排到它上方。c可能是0或者2,因为memory_order_release并不限制store操作之后的读写操作的重排。
memory_order_release and memory_order_consume
// thread A
c = 0; b = 0;
......
b = 1;
x.store(1, std::memory_order_release); // <----------------Point A
c = 2;
// thread B
while(x.load(std::memory_order_consume) == 1); // <----------------Point A
cout << b << c << endl;
cout << x << endl;
线程B的输出,b可能是1和0,因为b可能在x.load
之前就读取了;c可能是0或者2,因为它并没有被原子量限制读取顺序,x必然是1,因为x的值和x.load
有关,它不能被重排到x.load
上方
实践中如何使用
- 如果没搞清楚,就用默认的
memory_order_seq_cst
- 如果只需要原子性,不需要线程间同步,就用
memory_order_relaxed
。比如需要多线程共享一个计数器,但是并不需要该计数器绝对准确。
相关概念:内存屏障
之后的C++版本还引入了内存屏障,和内存序相似
acquire_memory_fence( void )
ensures that all subsequent operations in program order are performed after all preceding loads in program order;release_memory_fence( void )
ensures that all preceding operations in program order are performed before all subsequent stores in program order;acq_rel_memory_fence( void )
, combines the semantics of acquire and release;ordered_memory_fence( void )
, ensures that all preceding operations in program order are performed before all subsequent operations in program order.