std::memory_order_seq_cst工作机制及C++线程安全问题
C++多线程同步场景问题解答
基础场景定义
首先定义两个普通共享变量:
int val; Cls obj;
使用原子布尔变量作为数据就绪标识:
std::atomic_bool flag = false;
线程1(写线程)执行逻辑
仅负责写入共享变量,逻辑如下:
while (flag == true) { /* Sleep */ } val = ...; obj = ...; flag = true; /* 写完共享变量后置位flag */
线程2(读线程)执行逻辑
仅负责读取共享变量,逻辑如下:
while (flag != true) { /* Sleep */ } int local_val = val; Cls local_obj = obj; flag = false; /* 读完共享变量后复位flag */
问题1:默认std::memory_order_seq_cst内存序下,退出循环后读写共享变量是否线程安全?
是线程安全的。std::atomic_bool默认使用的std::memory_order_seq_cst是C++内存模型中最强的内存序,提供全局一致的全序同步保证,完全满足这个单写单读交替场景的同步要求:
- 对写线程而言,对普通共享变量
val、obj的写入操作,和后续flag = true的原子写操作之间存在严格的先后顺序约束,编译器和CPU都不会把flag的置位操作重排到两个普通变量写入之前。 - 对读线程而言,当循环检测到
flag == true退出时,内存序保证读线程可以完整观测到写线程在flag置位前的所有内存写入结果,也就是能拿到val和obj的最新有效值,不会读到脏数据。 - 同理,读线程对
val、obj的读取操作,和后续flag = false的原子写操作之间也不会被重排,不会出现写线程提前观测到flag为false、启动下一轮写入,和读线程的读取操作产生并发冲突的问题。 - 两个线程对
flag的原子操作本身有全局顺序保证,不会出现两个线程同时通过flag检查、同时操作普通共享变量的情况,天然实现了两个线程对临界区的互斥访问。
问题2:将std::atomic_bool替换为普通bool类型,代码逻辑是否正确?
完全不正确,属于标准层面的未定义行为,哪怕在部分平台上测试时看起来能正常运行,也只是巧合,没有任何可移植性保证,核心问题有两个:
- 首先触发明确的数据竞争:C++标准规定,只要存在多个线程并发访问同一个非原子内存位置,且至少有一个访问是写入操作,且没有任何同步原语做happens-before保证,就属于数据竞争,直接导致未定义行为。普通
bool的读写不是原子操作,两个线程循环读写同一个普通bool变量本身就已经违反规则,可能出现读写值撕裂、写入永久丢失等问题。 - 没有内存序约束,重排和可见性问题无法避免:就算抛开原子性问题,假设目标平台上单字节bool的读写天然是原子的,编译器和CPU依然可以自由做指令重排、寄存器缓存优化:
- 写线程可能把
flag = true的赋值重排到val、obj赋值之前,导致读线程看到flag为true时,val和obj还没完成写入,读到无效值。 - 读线程可能把循环中对flag的读取优化为寄存器缓存,只在循环开始读一次flag值,之后永远不重新从内存加载新值,哪怕写线程已经修改了flag,读线程也会永远卡在循环里。
- 读线程也可能把
val、obj的读取操作重排到flag状态判断之前,在flag还没被置位的时候就提前读了旧值。
- 写线程可能把
注意:x86等强内存序平台硬件层面不会做StoreStore、LoadLoad类的重排,但编译器依然可能在优化阶段做出上述重排,除非手动添加编译器屏障,否则依然可能出问题,这种写法在工程实践中是绝对禁止的。
内容的提问来源于stack exchange,提问作者guan boshen
相关产品推荐
相关产品推荐

