C++11引入了atomic和memory order支持,使得写可移植的无锁数据结构成为可能。
其中memory order支持两种形式的API,一种是在操作一个atomic变量时指定memory order,另外一种是单独指定memory order的atomic_thread_fence()函数调用.
- memory order主要有以下几种:
- memory_order_relaxed
只提供对单个atomic变量的原子读/写,不和前后语句有任何memory order的约束关系。这种情况往往使用于普通计数器,它甚至不能用来做引用计数器。因为引用计数器涉及对象析构,因为缺少内存栅栏作用,这可能导致别的CPU看不到对象内容最新数据而产生错误的析构行为。 - 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不能执行
x86不保证StoreLoad的顺序,一条Store指令在前,后面一条不相关的load指令可以先执行。因为这个顺序的不保证,导致Peterson lock实际上需要使用mfence指令才能在x86上实现。
x86上很多原子操作需要使用lock前缀或者隐含lock语义,例如xchg指令。这个lock语义是上面memory_order_seq_cst的语义,是一个full memory barrier。相对来说在x86上的memory order 比较容易使用,但是性能有所损失,例如上面的LoadLoad是顺序执行的,但是如果第一个Load因为cache不命中,就引起从内存Load而导致的延迟,虽然第二个Load是可以cache命中的,但是因为第一个Load的delay,影响到第二个Load的执行,继而导致后续运算都delay。
3.实践中碰到编译器的bug
考虑以下代码,是用来防止x86上的StoreLoad的reorder问题。
#include<atomic>
using namespace std;
atomic_int a;
int j;
int func()
{
int n;
a.store(1,memory_order_acquire);
n = j;
return n;
}
程序想先给a赋值1,然后读变量j的值。如果不强加memory order,则读j的指令可能会被cpu先执行,
这是用clang++编译后的结果:
可以看到,clang++并没有对memory order进行约束。
gcc编译的结果:
明显可以看到在中间加入了mfence指令,防止前后两条指令执行乱序。
从编译情况来看gcc正确理解了程序的意图,而clang++貌似理解错了。