单写多读场景下基础类型变量是否需用互斥锁/原子类型保护?
假设有大量线程,以及一个名为flag的普通、可平凡复制的非数组基础类型变量(如float、uint16_t等)。仅存在一个线程频繁为该变量赋值,其余线程仅读取变量值且不执行写入操作。在此场景下,是否必须将该变量设为原子类型或用互斥锁保护?已知多线程写入变量时必须进行保护,但此场景是否有必要?该要求是否与平台相关?
核心结论
必须进行保护,且该要求与平台(CPU架构、内存模型)强相关——不仅是标准合规性要求,更是避免实际运行中出现诡异bug的必要措施。
为什么需要保护?
可见性问题
现代CPU依赖多级缓存提升性能,单线程写入的flag值可能仅停留在当前CPU核心的缓存中,不会同步到主内存。其他线程所在的核心可能一直读取旧的缓存值,永远拿不到最新的写入结果。此外,编译器可能会对普通变量的读取做优化(比如把值缓存到寄存器中,不再重复访问内存),导致读线程持续读取过期值。部分读写风险
对于超过CPU总线宽度的类型(比如32位CPU上的64位变量),普通写入操作会被拆分为多次内存访问。读线程如果在两次访问的间隙读取,会拿到“半更新”的无效值。即使是和总线宽度匹配的类型,弱内存模型平台也不保证单个读写操作的原子性。
平台相关的差异
强内存模型平台(如x86/x86_64)
这类平台对对齐的基础类型(如32位int在32位对齐地址)的单个读写操作是原子的,但可见性问题依然存在。编译器的优化或缓存一致性机制的延迟,会导致读线程无法及时获取最新值。仅靠普通变量无法解决可见性问题,必须借助原子类型、内存屏障或互斥锁来强制同步缓存。弱内存模型平台(如ARM、PowerPC)
这类平台既不保证普通变量读写的原子性,也不保证缓存自动同步。比如32位ARM上写入64位变量会拆分为两次32位操作,读线程可能读到中间值;同时,缓存不会主动同步,读线程可能一直拿到旧值。这种场景下必须用原子类型或互斥锁来确保操作的安全性。
从C/C++标准角度
C/C++标准明确规定:普通变量的多线程读写(即使是单写多读)属于数据竞争,会触发未定义行为。无论平台实际表现如何,这种写法在标准层面是不合法的,必须通过原子类型或同步机制(如互斥锁)消除数据竞争。
最优实践
最稳妥且跨平台的做法是将flag声明为原子类型:
- C语言使用C11标准的
_Atomic修饰符,如_Atomic uint16_t flag; - C++语言使用
std::atomic模板,如std::atomic<uint16_t> flag;
原子类型既保证读写操作的原子性,又通过内存模型规则确保写操作的结果能被读线程及时可见,无需额外手动处理缓存或屏障。
内容的提问来源于stack exchange,提问作者postcoital-solitaire

