C++多线程中无锁写与加锁读的内存可见性问题
C++多线程中无锁写与加锁读的内存可见性问题
这个问题问得太戳痛点了,刚好是C++多线程里新手(甚至不少老鸟)容易踩的坑!咱们掰开揉碎了说:
首先给你拍板结论:线程B完全有可能看到set_ = false的陈旧值,哪怕它加了锁。别惊讶,咱们来拆解为什么:
核心误区:mutex的同步是有前提的
你以为线程B加了mutex_的锁,就能看到所有线程的最新写入?错!mutex的内存屏障(也就是保证可见性的关键),只对用同一把mutex做过“解锁-加锁”配对的操作生效。换句话说:
- 只有当线程A在写完
set_之后,解锁了mutex_,然后线程B再去加同一把mutex_,这时候B才能100%看到A写的最新值。 - 但你的代码里,线程A碰都没碰
mutex_,它的写操作是完全无锁的,和B的加锁操作之间没有任何同步关系。mutex的屏障管不到这种“野路子”的写操作。
为什么会看到陈旧值?
咱们从两个层面看:
- 编译器优化:编译器看到线程A里的
set_ = true没有任何同步约束,完全可能把这个写操作优化到A线程的寄存器里,根本不刷回主存——在编译器眼里,既然没有同步,这个变量就只会被A自己读写,干嘛费力气刷主存? - CPU缓存层面:现代CPU都是多核缓存,A核心的缓存里改了
set_,但如果没有触发缓存同步的信号(比如mutex解锁会发这个信号),B核心的缓存里可能还存着set_的旧值,而且不会主动去主存刷新——毕竟缓存一致性协议也不是实时同步所有数据,得有触发条件。
更要命的:你的代码其实有未定义行为!
哦对了,差点忘了说:普通bool set_这种非原子变量,跨线程无同步的读写(A无锁写,B加锁读但没和A同步),在C++标准里属于未定义行为。什么意思?编译器可以完全无视你的逻辑,比如直接把B里的if (!set_)整个优化掉,因为它认为没有同步的话,set_的值不可能被其他线程修改。这可不是危言耸听,O2/O3优化下真的会出现这种情况。
怎么修复?
给你两个靠谱的方案:
- 方案一:写操作也加锁:线程A的
setter()里也加std::unique_lock lock(mutex_);,这样A的解锁和B的加锁形成同步对,mutex的内存屏障就能保证B看到最新的set_值。 - 方案二:用原子变量替代普通bool:把
set_改成std::atomic<bool> set_ = false;,写的时候用set_.store(true, std::memory_order_release);,读的时候用if (!set_.load(std::memory_order_acquire))。这样不用mutex也能建立同步关系,保证可见性,性能还可能更好。哪怕你用最宽松的std::memory_order_relaxed,也比普通bool安全,至少不会有未定义行为。
最后再敲个黑板:跨线程的变量访问,要么用同一把锁同步读写,要么用原子变量,别搞这种“半锁半无锁”的操作,坑多到你防不胜防!




