C++原子操作总结

关于乱序

c++程序中,没有依赖关系的语句的执行顺序是无法预测的。

乱序的原因

  1. 编译器优化。编译器优化假设程序是单线程环境时,如果编译器出于某种原因会对程序执行顺序并且不会改变其结果,此时是可以乱序执行的。编译器一般不会将函数之后的指令移到函数前面,因为编译器一般不知道函数体内是否含有memory barrier
  2. 处理器优化。处理器允许指令乱序,这样避免指令等待资源而暂时hang住,处于闲置状态。eg. cache miss。如果当前指令cache miss,下一条命中,则可能会乱序执行。
  3. 存储系统。在每个cpu核中都存在一个store buffer(可以理解成一些特殊寄存器)。在一些写指令执行的时候,写的数据先存在各自的store buffer中,只有自己感知得到,这之后会以先进先出的方式往l2 cache等逐级向下到内存中。而存往store buffer时,cpu就已经认为这条指令执行结束了,因此可能产生乱序。

直面乱序

  1. 乱序执行不可避免
  2. 如果读写指令涉及的不是共享变量,则不会有坏影响
  3. 否则,应该用锁或者原子变量

一些常用原子操作技巧

condition lock

if (cond)
    mutex1.lock();
.....
if (cond)
    x = 1;
......
if (cond)
    mutex1.unlock();

其本身是安全的,但可能会触发编译器bug, 会导致在临界区外执行两次, 在多线程时会触发bug

if (cond)
    mutex1.lock();
.....
register1 = x;
x = 1;
if (!cond)
    x = register1;
......
if (cond)
    mutex1.unlock();

acquire && release

x=1;
lock;   // acquire lock
y=2;
unlock;  // release lock
z=3;

acquire 保证在它之后的指令绝对不会在它之前执行,release操作保证在它之前的指令绝对不会在它之后执行。

可以见下图
atomic1
并有承诺:release执行后,临界区里的写操作都已经写入内存,并已经全局可见。acquire执行前,其临界区里的读操作都不会执行。

实现

  1. 编译器:使用memory barrier 实现,并且使用汇编实现,这样可以排除编译优化
  2. 处理器:
    • PowerPC 中的lwsync指令,在流水线内属于一个双向barrier,遇到这个指令后必须等流水线的指令执行完之后才会继续执行。
    • Intel IA64中提出 st.rel(release store) 和 ld.acq (acquire load)指令,使得处理器乱序的可能性增强。

一些注意点

  1. 片上网络:On-chip Network, 指的是cpu核的数据从store buffer到l2一致性cache时,会对其他cpu核进行广播,来保证所有核对于一致性cache里的数据全都一致,单这个广播存在时间,并且有快有慢。因此可能会存在预期之外的结果。
  2. 因为片上网络的原因,acquire && release 不保证全局序。举个例子,初始x=y=0, cpu1: x=1, cpu2 y=1, 广播后,可能同时存在 cpu3: x=1,y=0 cpu4: x=0, y=1。
    4.. std::atomic使用于acquire/release更强的一致性协议consistence sequence,排除片上网络导致的全局序失效。(默认情况下)。consistence sequence 保证在一个广播结束之后,再进行下一个广播。