C++ 并发:内存序、可见性与指令重排
本文面向有一定 C++ 并发基础的读者(知道线程、互斥量、基本的 std::atomic 用法),但想把'为什么这样'弄清楚。我们会从 std::atomic 的语义出发,讲清 CPU cache coherence、内存屏障(fence)、指令重排 和 happens-before 的关系——不是空洞的定义,而是大量实战例子、容易踩的坑和调试技巧。
1. 为什么要理解内存模型?一个小实验
先给你一个看起来简单但会'出错'的例子:
int x = 0, y = 0;
void thread1(){
x = 1; // A
int r1 = y; // B
}
void thread2(){
y = 1; // C
int r2 = x; // D
}
直觉会告诉你 r1 == 0 && r2 == 0 不可能同时成立:因为若两个线程都先写后读,总有一个先写早于另一个后读。但在现实的多核处理器上,如果没有同步,两个读取同时得到 0 是可能的——因为写入对其他核可见需要时间,或编译器/CPU 做了重排。
这就是为什么我们不能把并发程序的正确性只交给直觉:你需要明确'一个操作对另一个操作是否可见'的约定,也就是 happens-before。
2. 可见性、顺序与一致性:先把名词搞清楚
三个最常见的术语:
- 可见性(visibility):一个线程对某个内存写入何时能被另一个线程观察到。
- 顺序(ordering):在执行流中的操作顺序,分为程序顺序(程序编写的顺序)、一致顺序(在某种语义下保证的顺序)。
- 一致性(consistency):当多线程都观察到内存时,是否满足我们期待的全局一致性(例如线性一致性/顺序一致性)。
硬件保证的通常是 缓存一致性(cache coherence)——同一地址的不同副本(存在于多个 cache 层)最终会保持一致。但这并不自动保证操作间的全局顺序性,也不防止编译器在不破坏单线程语义的前提下重排指令。
3. CPU 的缓存一致性(cache coherence)到底保不保底?
现代多核 CPU 通常实现 MESI(或其变体)协议来维护缓存一致性。
- MESI(Modified, Exclusive, Shared, Invalid)定义了缓存行在不同核心缓存间的状态转换,保证写入最终传播到其他核心。换句话说,CPU 层面把'同一地址不会无限分歧'这一事保证住了。
重要的限制:
- Cache coherence 是对单个内存地址的保证,而不是多个地址间的原子复合保证;
- 它并不提供跨地址的全序写可见性;也不约束指令重排。
举例:当线程 A 在地址 p 写 1,线程 B 立刻读 ,并不一定马上得到 1;缓存一致性保证最终能看到 1,但在没有内存屏障或原子操作的情况下'最终'可能对短时间窗口无保证。


