You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何需使用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关系,具体原因分两点:

  1. 非原子访问直接构成数据竞争,属于未定义行为
    两个线程并发访问同一个非原子变量y,且写线程修改y、读线程读取y,全程没有任何同步保护,已经直接触发C++标准定义的数据竞争,程序行为不受任何保证,出现任何结果都符合标准。
  2. 栅栏同步完全失效,重排和优化会破坏逻辑预期
    • 配对规则要求: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 02:39:17