携带依赖的表达式中子表达式的内存序问题
结论
两个断言都存在数据竞争,均无法保证符合C++标准语义下的通过要求。
具体解释
示例代码
int data = 0; std::atomic<int> ready = 0; void writer_thread() { data = 25; ready.store(1, std::memory_order::release); } void reader_thread() { int ready_value = 0; while(!ready_value) { ready_value = ready.load(std::memory_order::consume); } assert(data == 25); // #1 assert(data + ready_value == 26); // #2 }
断言#1的问题
该断言失败的原因比较明确:data的读取完全不依赖ready.load的返回值,不在memory_order_consume的可见性保护范围内,完全可能读到未更新的初始值0,触发断言失败。
断言#2的问题
data + ready_value == 26整个表达式携带ready.load的依赖,不代表其内部所有子表达式的求值都受可见性保护,核心原因有两点:
- 依赖传递方向不覆盖无关联子表达式
标准规定的依赖传递是从操作数到上层表达式的单向传递:表达式会携带其操作数的依赖,但反过来不成立,上层表达式有依赖不代表所有下层子表达式都依赖于load结果。data子表达式本身和ready_value没有任何数据依赖,它的求值不需要用到ready.load的返回值,所以不受consume的可见性规则保护。 - 操作数求值顺序未指定
C++标准没有规定二元运算符的两个操作数的求值顺序,编译器完全可以先求值data,再求值ready_value。这时候求值data的时候,ready.load的依赖链还没有建立,writer_thread对data的写入完全没有可见性保证,仍然可能读到0,最终0 + 1 = 1,触发断言失败。
注:目前绝大多数主流编译器都未严格实现memory_order_consume的原始语义,而是直接将其降级为memory_order_acquire处理,在这类实现下两个断言都能稳定通过,但这属于编译器的实现扩展,不符合C++标准对memory_order_consume的语义定义。
补充:
memory_order_consume的可见性保证仅作用于求值逻辑直接依赖load返回值的操作。比如把代码改成ready存data的指针,reader端用consume load拿到指针后再解引用读取值,这时候解引用操作直接依赖load得到的指针值,才会受到可见性保护,能保证读到writer写入的25。
内容的提问来源于stack exchange,提问作者md1357
相关产品推荐
相关产品推荐

