关于条件性依赖原子加载的原子存储:硬件重排、推测执行及C++内存模型保障的技术问询
各位大佬好,我最近在啃C++原子操作和内存模型的内容,碰到一个绕不开的困惑,想请教大家:
如果有这样一段逻辑:先对原子变量x做std::memory_order_relaxed加载,然后通过if语句判断加载结果,只有满足条件时才对另一个原子变量y做std::memory_order_relaxed存储——这种情况下,硬件会不会把加载和存储的顺序重排?或者会不会因为推测执行,在if条件还没确认成立时就把y的存储执行了,之后发现条件不成立又没法回滚,导致其他线程读到错误的y值?
这事儿直觉上绝对不该发生,毕竟要是线程1因为分支预测之类的原因,提前把y=1写出去,之后发现条件其实不成立,根本没法撤销这个写入——别的线程或者DMA设备可能已经读到这个错误值了!
我一开始也琢磨过用acquire/release语义,但仔细想了想好像不对症:release语义是用来让当前线程之前的写入,对读到这个release存储的线程可见,但这里x不是线程1写的,线程1完全没法通过操作y来影响x在其他线程的可见性。而且我翻C++文档里的acquire/release例子,全都是“一个纯写线程+一个纯读线程”的配对场景,完全没涉及这种“读-条件判断-跨变量写”的情况。
先贴一下我最开始的基础代码片段:
std::atomic<int> x; std::atomic<int> y; void thread1() { int val = x.load(std::memory_order_relaxed); if( val == 42 ){ y.store(1, std::memory_order_relaxed); } }
后来有人提醒我要补充涉及其他线程的例子,我整理了两个场景,写完之后对问题也更清晰了,现在把问题拆成更明确的两个角度:
如果是relaxed原子加载之后跟着一个relaxed原子存储(目标是另一个变量),且存储完全依赖于加载结果的条件判断(就是上面thread1做的事):
- 这个if语句会不会相当于隐式建立了一个load-store栅栏,要求加载必须完全完成,存储才能对其他线程(或DMA这类观察实体)可见?
- 用C++内存模型的术语说,这个if语句会不会在加载和存储之间建立
happens-before关系?
烦人的是,我翻了一圈C官方文档,完全没找到关于条件性原子存储的说明。按C的形式化定义,happens-before一般是通过release存储来建立的,但release还会阻止存储-存储重排,比我需要的约束更严格——如果if语句本身就能起到load-store栅栏的作用,那不用显式栅栏反而更高效。我总觉得真实硬件上肯定是按“加载完成再存储”来的,但C++文档给的保障好像比实际硬件弱,允许了一些现实中根本不会发生的行为?
示例1:观察者视角的存储顺序问题
这个例子是想搞清楚,其他线程会不会感知到y的存储先于x的加载完成:
std::atomic<int> x; std::atomic<int> y; void thread1() { int val = x.load(std::memory_order_relaxed); if( val == 42 ){ y.store(1, std::memory_order_relaxed); } } void thread2() { x.store(42, std::memory_order_relaxed); } void thread3() { if( y.load(std::memory_order_relaxed) == 1 ){ std::atomic_thread_fence(memory_order_acquire); int r = x.load(std::memory_order_relaxed); assert(r == 42); // 这个断言会不会失败? } }
示例2:推测执行导致的错误存储问题
这个例子更聚焦于推测执行会不会触发本不该发生的存储:
std::atomic<int> x; std::atomic<int> y; std::atomic<bool> terminate; void thread1() { while( !terminate.load(std::memory_order_relaxed) ){ int val = x.load(std::memory_order_relaxed); if( val == 42 ){ // 这里的y.store会不会被推测执行? y.store(1, std::memory_order_relaxed); break; } } } void thread2() { // 这个线程会从10000000往下写x,但绝对不会写到42 for(int idx = 10000000; idx > 42; --idx){ x.store(idx, std::memory_order_relaxed); } sleep_seconds(10); terminate = true; } void thread3() { while( !terminate.load(std::memory_order_relaxed) ){ if( y.load(std::memory_order_relaxed) == 1 ){ // 这事儿绝对不该发生! Open_Pandoras_Box(); Launch_Nuclear_Missiles(); Unleash_Armageddon(); break; } } }
真心希望各位大佬能帮忙解惑,谢谢大家!
内容来源于stack exchange

