关于乱序
c++程序中,没有依赖关系的语句的执行顺序是无法预测的。
乱序的原因
- 编译器优化。编译器优化假设程序是单线程环境时,如果编译器出于某种原因会对程序执行顺序并且不会改变其结果,此时是可以乱序执行的。编译器一般不会将函数之后的指令移到函数前面,因为编译器一般不知道函数体内是否含有memory barrier
- 处理器优化。处理器允许指令乱序,这样避免指令等待资源而暂时hang住,处于闲置状态。eg. cache miss。如果当前指令cache miss,下一条命中,则可能会乱序执行。
- 存储系统。在每个cpu核中都存在一个store buffer(可以理解成一些特殊寄存器)。在一些写指令执行的时候,写的数据先存在各自的store buffer中,只有自己感知得到,这之后会以先进先出的方式往l2 cache等逐级向下到内存中。而存往store buffer时,cpu就已经认为这条指令执行结束了,因此可能产生乱序。
直面乱序
- 乱序执行不可避免
- 如果读写指令涉及的不是共享变量,则不会有坏影响
- 否则,应该用锁或者原子变量
一些常用原子操作技巧
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操作保证在它之前的指令绝对不会在它之后执行。
可以见下图
并有承诺:release执行后,临界区里的写操作都已经写入内存,并已经全局可见。acquire执行前,其临界区里的读操作都不会执行。
实现
- 编译器:使用memory barrier 实现,并且使用汇编实现,这样可以排除编译优化
- 处理器:
- PowerPC 中的lwsync指令,在流水线内属于一个双向barrier,遇到这个指令后必须等流水线的指令执行完之后才会继续执行。
- Intel IA64中提出 st.rel(release store) 和 ld.acq (acquire load)指令,使得处理器乱序的可能性增强。
一些注意点
- 片上网络:On-chip Network, 指的是cpu核的数据从store buffer到l2一致性cache时,会对其他cpu核进行广播,来保证所有核对于一致性cache里的数据全都一致,单这个广播存在时间,并且有快有慢。因此可能会存在预期之外的结果。
- 因为片上网络的原因,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 保证在一个广播结束之后,再进行下一个广播。