为何仅std::memory_order_seq_cst能保证断言不触发?
先明确cppreference那个易混淆示例的核心场景:四个线程,两个分别写入原子变量x、y,另外两个线程分别先等待x/y完成写入,再读取另一个变量,若未读取到新值则影响z的状态。使用std::memory_order_seq_cst时,断言z.load() != 0永远不会触发;但换成acquire/release内存顺序后,断言可能触发——也就是z可能保持为0。
原因在于acquire/release和seq_cst的核心差异,并非acquire/release的同步或重排限制失效,而是两者的约束范围完全不同:
1. acquire/release仅保证单一原子变量的同步,不约束全局操作顺序
acquire和release的配对只做一件事:对同一个原子变量,release操作之前的所有写操作,对后续执行acquire操作的线程可见,同时禁止线程内的重排(release前的操作不能排到release后,acquire后的操作不能排到acquire前)。但它完全不关心不同原子变量的操作之间的全局顺序。
放到示例中:
- 线程A的
x.store(1, release)和线程C的x.load(acquire)建立了同步:线程A的x写入对线程C可见,且线程C中读x之后的读y操作不会被重排到读x之前。 - 线程B的
y.store(1, release)和线程D的y.load(acquire)同理。
但acquire/release没有强制要求x.store和y.store在全局中有明确的先后顺序。CPU可能让这两个写入操作在各自的缓存中独立完成,从全局视角看是“同时”发生的。这时会出现极端情况:
- 线程C读到x=1后,去读y时,线程B的y写入还没同步到线程C所在的核心,读到的是0;
- 同时线程D读到y=1后,去读x时,线程A的x写入还没同步到线程D所在的核心,读到的是0。
这种情况下,两个读取线程都不会触发z的非0赋值,导致z保持初始的0状态,触发断言。
2. acquire的重排限制是线程内的,不解决全局顺序问题
你提到std::memory_order_acquire防止代码重排到load之前,这一点没错,但它的约束只限于当前线程内部——它保证线程内acquire后的操作不会跑到acquire前面,但管不了其他线程的操作顺序,也管不了当前线程读到的值在全局中的“位置”。
比如线程C的读y操作不会被重排到读x之前,但这并不意味着线程C读到的y一定是最新的。因为acquire/release没有强制x和y的写入操作在全局中有先后,所以线程C和线程D可能各自看到完全相反的操作顺序:线程C觉得x先被写入,y还没;线程D觉得y先被写入,x还没。这种“各说各话”的情况,就会导致z的状态不符合seq_cst下的预期。
而std::memory_order_seq_cst的核心是全局总顺序:所有原子操作都遵循一个单一的全局顺序,所有线程看到的操作顺序都是一致的。要么x的写入在y之前,要么y的写入在x之前,绝不会出现两个线程看到相反顺序的情况。因此无论哪种情况,至少有一个读取线程会读到另一个变量的新值,从而保证z被设置为非0,断言永远不会触发。
内容的提问来源于stack exchange,提问作者xmh0511

