C++ 并行编程之原子操作的内存顺序
1. C++原子操作的内存顺序概述
- memory order主要有以下几种:
-
memory_order_relaxed
只提供对单个atomic变量的原子读/写,不和前后语句有任何memory order的约束关系。 -
memory_order_consume
程序可以说明哪些变量有依赖关系,从而只需要同步这些变量的内存。
类似于memory_order_acquire,但是只对有依赖关系的内存。意思是别的CPU执行了memory_order_release操作,而其他依赖于这个atomic变量的内存会被执行memory_order_consume的CPU看到。这个操作是C++特有的,x86也不支持这种类型的memory order,不清楚其他种类的cpu是否支持,这还涉及到编译器是否支持这种细粒度控制,也许它直接按memory_order_acquire来处理。 -
memory_order_acquire
执行memory_order_acquire的cpu,可以看到别的cpu执行memory_order_release为止的语句对内存的修改。执行memory_order_acquire这条指令犹如一道栅栏,这条指令没执行完之前,后续的访问内存的指令都不能执行,这包括读和写。 -
memory_order_release
执行memory_order_release的cpu,在这条指令执行前的对内存的读写指令都执行完毕,这条语句之后的对内存的修改指令不能超越这条指令优先执行。这也象一道栅栏。
在这之后,别的cpu执行memory_order_acquire,都可以看到这个cpu所做的memory修改。 -
memory_order_acq_rel
是memory_order_acquire和memory_order_release的合并,这条语句前后的语句都不能被reorder。 -
memory_order_seq_cst
这是比memory_order_acq_rel更加严格的顺序保证,memory_order_seq_cst执行完毕后,所有其cpu都是确保可以看到之前修改的最新数据的。如果前面的几个memory order模式允许有缓冲存在的话,memory_order_seq_cst指令执行后则保证真正写入内存。一个普通的读就可以看到由memory_order_seq_cst修改的数据,而memory_order_acquire则需要由memory_order_release配合才能看到,否则什么时候一个普通的load能看到memory_order_release修改的数据是不保证的。
- x86的memory order
x86的memory order是一种strong memory order,它保证:
- LoadLoad是顺序的
一个cpu上前后两条load指令是顺序执行的,前面一条没执行完毕,后面一条不能执行 - StoreStore是顺序的
一个cpu上前后两条store指令是顺序执行的,前面一条没执行完毕,后面一条不能执行 - LoadStore
一个cpu上前面一条是Load指令,这条指令没执行完毕,后面一条store不能执行
2. 为什么需要定义这样内存顺序
简单的解释,就是cache的存在,虽然前面更新了变量的值,但是可能存在某个CPU的缓存中,而其他CPU的缓存还是原来的值,这样就可能产生错误,定义内存顺序就可以强制性的约束一些值的更新。
为什么需要这么多类型?灵活度更大,合理的使用可以最大化利用系统的性能。
3. 内存操作的种类
- Store operations —— 写
which can have memory_order_relaxed , memory_order_release ,or memory_order_seq_cst ordering - Load operations —— 读
which can have memory_order_relaxed , memory_order_consume ,memory_order_acquire, or memory_order_seq_cst ordering - Read-modify-write operations —— 读-修改-写
which can have memory_order_relaxed , memory_order_consume , memory_order_acquire , memory_order_release , memory_order_acq_rel, or memory_order_seq_cst ordering
4. 内存模型关系
为什么要理解这个,其实这就是定义内存顺序的目的,因为有了这些关系的需求,才要定义这些顺序。
-
happens-before
对同一线程,就是一个操作要在另一个操作之前执行。对多个线程,某一个线程的操作A要在另一线程的操作B之前发生。若顺序变了,可能就不能达到我们希望的效果。 -
synchronizes-with
某一线程的load操作,依赖于另一线程的store操作,简单说,就是线程A读取的变量值a,应该是线程B修改后的值。
5. C++定义的内存顺序分别可以实现哪些关系?
-
relaxed 松散顺序
memory_order_relaxed 由这个值指定的,这个很简单,就是各个CPU读取的值是未定义的,一个CPU在一个线程中修改一个值后,其他CPU不知道。 -
sequentially-consistent 顺序一致顺序
memory_order_seq_cst 由这个值指定,这个也很简单,相当于各CPU的原子操作都是在一个线程上工作,一个修改后,其他CPU都会更新到新的值。
如下例,在此顺序下,只要x,y的值修改了,其他线程的load操作就会是最新的值,所以线程c和d,不管哪个先执行完,后执行完的肯定会修改z,所以一定不会报异常。但是要是松散模型,即使x,y变了,但是在线程cd load时,也可以是之前为未修改的值。
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x,y;
std::atomic<int> z;
void write_x() //a
{
x.store(true,std::memory_order_seq_cst);
}
void write_y() //b
{
y.store(true,std::memory_order_seq_cst);
}
void read_x_then_y() //c
{
while(!x.load(std::memory_order_seq_cst)); // 1
if(y.load(std::memory_order_seq_cst)) // 2
++z;
}
void read_y_then_x() //d
{
while(!y.load(std::memory_order_seq_cst)); // 3
if(x.load(std::memory_order_seq_cst)) // 4
++z;
}
int main()
{
x=false;
y=false;
z=0;
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);
}
-
acquire-release 获得-释放顺序
acquire操作(load)—— memory_order_consume , memory_order_acquire
release操作(store)—— memory_order_release
acquire-release操作 —— memory_order_acq_rel (先acquire再做一次release,对于fetch_add这样的操作,既要load一下,又要store一下的,就用这个)
一个原子做了这个acquire操作,肯定是读的是这个原子最后一次release操作修改的值。换句话说,要是采用release的方式store一个值,那么其他CPU都会看到这一次的修改。
(不举例子了,C++并发编程实战这本书上有,理解后再去看还是比较简单的) -
memory_order_consume 和 memory_order_acquire 有什么区别?
memory_order_consume: 只能保证当前的变量,不能保证此线程中此load操作语句之前的其他变量的操作也被其他CPU看到,也就是只能保证自己在其他CPU上都更新了。(这样说不准确,应该做可以保证当前变量,以及此线程中此语句之前的,并且与当前变量有依赖关系的变量的修改被其他CPU看到)
memory_order_acquire:不仅能保证当前变量的修改被其他CPU看到,也能保证这条语句之前的其他变量的load操作的值,也被其他CPU看到,不论其他变量是什么顺序的(relax的也会更新)。
参考
[1] 知乎上的这个回答,写的很好: 如何理解 C++11 的六种 memory order? - 知乎
[2] C++11 memory order-博客-云栖社区-阿里云
[3] 官方文档写的还可以(当我理解了,才觉得很好) std::memory_order - cppreference.com
[4] 《C++并发编程实战》或者《C++ Concurrency in Action》