1. happens‑before(先行发生)完整介绍
happens‑before 是 C++11 内存模型的逻辑时序关系,不是机器指令时序,不是CPU硬件时钟时序。
如果操作A happens‑before 操作B:
- A产生的所有内存副作用(写内存),对B全部可见;
- 在逻辑模型上A发生在B之前;
- 但底层机器指令可以重排,只要不违反这个逻辑上的约束。
⚠️重要:happens‑before只定义逻辑约束 ,不强制机器指令严格按这个顺序执行。
产生 happens‑before 的全部来源
① 程序顺序(线程内先行发生)Program Order
同一个线程,源码中A写在B前面 → A happens‑before B。
这是基础公理,不需要任何原子、不需要屏障。
cpp
int a,b;
a = 1; // A
b = 2; // B
// A happens‑before B
注意:编译器/CPU可以调换机器指令,只要本线程可观测行为不变。逻辑上依然A happens‑before B。
② 互斥锁 std::mutex
unlock() happens‑before 后续同一个锁的 lock()。
线程1解锁,线程2拿到同一把锁;线程1锁区内所有写,线程2锁区内全部可见。
③ 原子变量内存序配对(release‑acquire)
线程A:
atomic_var.store(val, memory_order_release)线程B:当读到该val,使用
atomic_var.load(memory_order_acquire)⇒ A的store happens‑before B的load。
同时:A中所有在store之前的内存写,对B中load之后全部可见。
线程B有两种情况:
- B 的
load(acquire)读到了 Astore(release)写入的 val
→ 条件成立:建立跨线程 happens‑before,A store 之前的普通写对 B load 之后可见。✅ - B 的
load(acquire)读到的是旧的初始值(还没看到 A 的写入)
→ 条件不成立!不会建立和 A 的 store 的 happens‑before。 什么附加可见性都没有。
cpp
std::atomic<bool> flag{false};
int data = 0;
//A线程
data = 100;
flag.store(true, std::memory_order_release);
//B线程,load(acquire) 是非阻塞读取,读到啥就返回啥
bool v = flag.load(std::memory_order_acquire);
if(v)
{
int tmp = data; // 只有 v==true(读到A写的true),data才保证100
}
else
{
//读到旧false,配对不触发,这里访问data没有任何同步保证
}
关键点:必须真正读到release写入的值才会建立 happens‑before;如果load读到旧值,不会建立happens‑before。
load(acquire) 是非阻塞读取:读到啥就返回啥;读到旧值就直接返回旧值,不会原地自旋等待 A 写入。
acquire 只是**当碰巧读到 release 写入的值时,激活同步效果;**它本身不做等待。
④ seq_cst 顺序一致
所有memory_order_seq_cst原子操作存在一个全局总序;在这个总序里靠前的操作 happens‑before 靠后的。
⑤ std::thread::join
th.join() 返回,则被join线程的全部操作 happens‑before join之后的代码。
⑥ std::atomic_thread_fence 配对
- 线程A:普通写 →
thread_fence(release)→atomic.store(...,relaxed) - 线程B:
atomic.load(...,relaxed)读到该值 →thread_fence(acquire)→ 普通读
配对成功,A fence之前写 happens‑before B fence之后读。
⑦ 传递性
如果 A happens‑before B,B happens‑before C,则 A happens‑before C。
什么情况下没有happens‑before
- 两个不同线程,没有锁、没有release/acquire配对、没有join;普通变量一写多读。→ 数据竞争,UB。
store(release)+load(relaxed):就算读到release写入的值,不会建立跨线程happens‑before。
release只标记输出点;relaxed load不会"捕获"这个先行发生关系。
数据竞争定义:两个不同线程,无happens‑before约束下,同时访问同一个非原子对象,至少一个是写 → UB。
2. happens‑before 对「代码序」、「指令序」的影响
先把三个概念分清:
- 代码序(源码序):你写C++文本从上到下的顺序。
- happens‑before序:C++抽象模型的逻辑时序。
- 指令序(机器指令序):编译器输出汇编,CPU实际执行的顺序。
规则:
代码序 → 推导得到 happens‑before序;
happens‑before序 约束指令序,但不等于指令序。
2.1 线程内:程序顺序带来的 happens‑before
cpp
//源码(代码序)
data = 100; //A
flag.store(true); //B
- 代码序:A 在 B前面。
- 所以:
A happens‑before B(线程内公理)。
👉 对**指令序(汇编)**的约束:
编译器、CPU可以重排机器指令,只要重排不会破坏本线程可观测行为。
例子:
cpp
int x=1;
int y=2;
int z = x + y;
代码序:x=1,y=2,z=x+y
happens‑before:x=1 h‑b y=2 h‑b z=x+y
编译器可以把汇编写成先y=2,再x=1。
指令序变了,但不破坏happens‑before逻辑,单线程结果不变,合法。
重点:线程内happens‑before不强制机器指令和源码一模一样。
2.2 引入 memory_order_release,改变对指令序的约束
cpp
data = 100; //A
flag.store(true, memory_order_release); //B
//A happens‑before B(线程内程序顺序)
memory_order_release 在原有线程内h‑b之上,增加额外重排约束:
禁止编译器+CPU,把A这类fence前的内存写,重排到store(B)之后。
注意:不是因为线程内h‑b不够;而是这个store要作为跨线程同步出口点 。如果允许把A重排到B之后,别的线程通过acquire看到B=true时,A还没完成,就破坏跨线程语义。
- x86‑64:release只约束编译器,不生成额外CPU硬件指令。
- ARM:编译器约束 + 插入硬件屏障dmb。
2.3 跨线程:happens‑before由release‑acquire配对建立
cpp
//生产者线程
data = 100; //W
flag.store(true, std::memory_order_release); //S
//消费者线程
if( flag.load(std::memory_order_acquire) ) //L
{
int v = data; //R
}
当L读到S写入的true:
S happens‑before L(配对产生跨线程h‑b)- W happens‑before S(生产者线程内程序顺序)
- 传递性:W happens‑before R
语义效果:
- W的写对R可见;
- 约束:W不能被重排到S之后;R不能被重排到L之前。
如果消费者是 load(relaxed)
cpp
if( flag.load(std::memory_order_relaxed) )
{
int v = data;
}
即便读到flag=true:
- W happens‑before S 依然成立(生产者线程内)
- ❌ S happens‑before L 不会建立
- 传递链条断裂,W does NOT happens‑before R
C++层面UB;硬件上即便没有任何指令重排,仅仅Store‑Buffer可见性延迟,也可能读到旧data。
关键点:线程内部天然的happens‑before,不会自动穿越线程边界 ;
需要release‑acquire配对把这个关系"传递"到另一个线程。
2.4 happens‑before 到底允许/禁止哪些重排?
把约束翻译成对指令序的规则:
- 如果A happens‑before B:
- 逻辑模型上A在B前面;
- 机器指令可以乱序,只要从所有线程的观察视角,不表现出"B完成而A未完成"。
- 线程内:只要本线程看不出变化,指令随便调。
- 跨线程有h‑b约束:不能出现"后发生的操作已经对外可见,先发生的操作还不可见"。
happens‑before 不是禁止一切重排;禁止的是"对外暴露破坏逻辑时序的现象"。
2.5 seq_cst 的特殊情况
memory_order_seq_cst 除了拥有release/acquire语义,额外增加:
全部seq‑cst原子操作存在一个全局总序 ,所有线程看到同一套顺序。
这会进一步限制指令重排,x86会生成mfence/锁前缀;ARM会生成较重dmb屏障。
release‑acquire 没有全局总序,只约束配对的两个线程,其他线程可以看到不同操作顺序。
核心总结(便于记忆)
-
happens‑before是逻辑时序,不是机器指令时序。
不等于汇编指令顺序,不等于CPU执行顺序。
-
线程内:源码顺序直接给出happens‑before;机器指令允许重排,但不能改变本线程可观测结果。不需要内存屏障。
-
线程内的happens‑before不会自动跨线程传播。
需要锁 / release‑acquire / join 等手段建立跨线程h‑b链条。
-
memory_order_release:约束:本线程前面内存访问不能重排到store之后;创造一个可以被acquire捕获的同步出口。
-
memory_order_acquire:约束:本线程后面内存访问不能重排到load之前;捕获对方release,建立跨线程happens‑before链条。
-
只要缺少跨线程happens‑before,哪怕机器没有发生任何指令重排,仅仅缓存/StoreBuffer可见性延迟,依然属于C++标准下的数据竞争UB。
通俗比喻:
happens‑before 是法律条文(C++标准);
编译器、CPU是现实执行机构,可以内部优化(重排指令),但对外行为不能违反法律条文。
如果你需要,我可以给一张极简示意图:展示「程序顺序 → happens‑before链条 → 对汇编指令的约束」。