为何需使用atomic<bool>避免数据竞争?非原子bool为何触发断言失败
《C++ Concurrency in Action》代码清单5.13 非原子变量导致断言触发问题解析
问题背景
阅读Antony Williams所著《C++ Concurrency in Action》代码清单5.13时,对注释*“对y的存储与加载操作必须是原子的,否则y上会发生数据竞争”*存在疑问:注释指出若y为普通非原子bool类型,程序中的断言可能触发,需要明确背后原理。
书中原代码(y为原子类型,断言永远不会触发)如下:
#include <atomic> #include <thread> #include <assert.h> bool x=false; std::atomic<bool> y; std::atomic<int> z; void write_x_then_y() { x=true; std::atomic_thread_fence(std::memory_order_release); y.store(true,std::memory_order_relaxed); } void read_y_then_x() { while(!y.load(std::memory_order_relaxed)); std::atomic_thread_fence(std::memory_order_acquire); if(x) ++z; } int main() { x=false; y=false; z=0; std::thread a(write_x_then_y); std::thread b(read_y_then_x); a.join(); b.join(); assert(z.load()!=0); }
修改后的测试代码
将y改为普通非原子bool类型后的代码如下,该版本下断言存在触发可能:
#include <atomic> #include <thread> #include <assert.h> bool x=false; bool y=false; std::atomic<int> z; void write_x_then_y() { x=true; std::atomic_thread_fence(std::memory_order_release); y=true; } void read_y_then_x() { while(!y); std::atomic_thread_fence(std::memory_order_acquire); if(x) ++z; } int main() { x=false; y=false; z=0; std::thread a(write_x_then_y); std::thread b(read_y_then_x); a.join(); b.join(); assert(z.load()!=0); }
原有认知偏差
- 已知非原子全局变量会引发数据竞争,但直觉上如果
read_y_then_x内的while循环退出,说明y已经在write_x_then_y线程中被设为true,或处于赋值过程中 - 认为
write_x_then_y中的std::atomic_thread_fence(std::memory_order_release)可以保证栅栏前的代码不会重排到栅栏之后,因此x=true必然已经执行完成 - 认为两个线程配对的release/acquire栅栏可以保证读取x时,x的更新已经和
read_y_then_x线程建立synchronized-with同步关系,断言应该始终成立
核心遗漏知识点
你漏掉的最关键规则是:C++内存模型中,release栅栏和acquire栅栏的同步配对,必须依赖同一个原子变量的存储-加载对作为锚点,非原子变量的访问完全无法触发栅栏间的synchronized-with关系,具体原因分两点:
- 非原子访问直接构成数据竞争,属于未定义行为
两个线程并发访问同一个非原子变量y,且写线程修改y、读线程读取y,全程没有任何同步保护,已经直接触发C++标准定义的数据竞争,程序行为不受任何保证,出现任何结果都符合标准。 - 栅栏同步完全失效,重排和优化会破坏逻辑预期
- 配对规则要求:release栅栏之后必须存在一个原子变量的存储操作,acquire栅栏之前必须存在同一个原子变量的加载操作,且加载读到了存储写入的值,两个栅栏才会建立同步关系,保证release栅栏前的所有写操作对acquire栅栏后的读操作可见。y改成非原子类型后,这个锚点直接消失,两个栅栏完全没有关联,根本不会生效。
- 编译器可以合法对非原子访问做优化:比如读线程的
while(!y);,因为y是非原子变量,编译器会默认没有其他线程会修改y,直接把循环优化为「只读取一次y的值,如果初始为false就进入死循环」,根本不会感知到y后续被写为true,导致线程b永远无法退出循环。 - 就算编译器没有做循环优化,CPU和编译器也可以合法把
x=true的写操作重排到y=true之后——因为release栅栏的顺序保证只针对原子操作锚定的同步场景,没有原子锚点时,这个顺序约束对另一个线程完全不生效,读线程完全可能先读到y=true,之后才读到x的写入,此时x还是false,z不会递增,最终触发断言。
内容的提问来源于stack exchange,提问作者manu652
相关产品推荐
相关产品推荐

