关于C++ Acquire/Release语义下4线程断言触发的困惑
我最近在读Anthony Williams的《C++ Concurrency in Action》,书中这段代码的断言z != 0居然可能触发,但我梳理了好几种执行路径都觉得断言不可能失败,实在搞不懂其中的逻辑——尤其疑惑y的store和read_x_then_y的load、x的store和read_y_then_x的load之间到底有没有同步关系。先贴出代码:
#include <atomic> #include <thread> #include <assert.h> std::atomic<bool> x,y; std::atomic<int> z; void write_x() { x.store(true,std::memory_order_release); } void write_y() { y.store(true,std::memory_order_release); } void read_x_then_y() { while(!x.load(std::memory_order_acquire)); if(y.load(std::memory_order_acquire)) ++z; } void read_y_then_x() { while(!y.load(std::memory_order_acquire)); if(x.load(std::memory_order_acquire)) ++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语义的理解,我来拆解清楚这个问题:
一、断言触发时的具体执行路径
核心要明确:Release-Acquire语义只保证「对同一个原子变量」的操作间的同步,不同原子变量的操作链是完全独立的,没有强制的全局同步关系。
触发断言的执行顺序是这样的:
- 线程
c(read_x_then_y):一开始加载x,此时线程a还没完成x.store(true, release),所以进入循环等待;等线程a完成x的store后,线程c退出循环,紧接着加载y——但此时线程b的y.store(true, release)还没同步到线程c所在的CPU缓存,y.load()返回false,所以z不递增。 - 线程
d(read_y_then_x):一开始加载y,此时线程b还没完成y.store(true, release),进入循环等待;等线程b完成y的store后,线程d退出循环,紧接着加载x——但此时线程a的x.store(true, release)还没同步到线程d所在的CPU缓存,x.load()返回false,所以z也不递增。 - 最终所有线程执行完毕,
z仍然是0,断言触发。
你之前的疑惑点可能在这里:线程a的x.store和线程d的x.load之间没有同步关系——线程d的同步点是基于y的Acquire操作,它只保证y.load(acquire)之后的操作能看到y.store(release)之前的内容,和x的状态完全无关。同理,线程c的Acquire基于x,和y的操作也没有绑定同步。
二、为什么顺序一致性下断言永不触发
如果把所有原子操作的内存序改成std::memory_order_seq_cst,断言就永远不会失败。因为顺序一致性要求所有原子操作在全局视角下有一个单一的总执行顺序:
- 要么
x.store(true)在y.store(true)之前:那么线程d在看到y为true时,必然能看到更早执行的x.store的结果,会递增z; - 要么
y.store(true)在x.store(true)之前:那么线程c在看到x为true时,必然能看到更早执行的y.store的结果,会递增z;
不管哪种情况,z至少会被递增一次,断言永远成立。
而Release-Acquire没有全局总顺序的约束,它只保证单个变量的操作链上的同步,不同变量的操作可以在不同CPU缓存中“各自为政”,出现互相看不到的情况。
三、你的推理逻辑可能存在的错误
你大概率是默认了不同原子变量的Release-Acquire操作之间也会产生全局同步,但实际上:
write_x中的x.store(release)只会和通过x.load(acquire)获取到这个值的线程(也就是线程c)同步,这个同步只保证线程c能看到线程a在x.store之前的操作,和y的状态无关;- 线程
c的y.load(acquire)和线程b的y.store(release)之间没有必然同步——线程c只是退出x的循环后去加载y,并没有等待b的store完成并同步到自己的缓存; - 同理,线程
d的x.load(acquire)和线程a的x.store(release)之间也没有强制同步关系。
简单总结:Release-Acquire是“点对点”的局部同步,不是“全局统一”的同步,不同变量的同步链互相独立,这就导致了两个读取线程都看不到另一个写入线程的操作,最终z保持为0的极端情况。
内容的提问来源于stack exchange,提问作者Aryan

