【C++11】内存模型的逻辑时序关系 happens‑before

1. happens‑before(先行发生)完整介绍

happens‑before 是 C++11 内存模型的逻辑时序关系,不是机器指令时序,不是CPU硬件时钟时序。

如果操作A happens‑before 操作B:

  1. A产生的所有内存副作用(写内存),对B全部可见;
  2. 在逻辑模型上A发生在B之前;
  3. 底层机器指令可以重排,只要不违反这个逻辑上的约束。
    ⚠️重要: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有两种情况

  1. B 的 load(acquire) 读到了 A store(release) 写入的 val
    → 条件成立:建立跨线程 happens‑before,A store 之前的普通写对 B load 之后可见。✅
  2. 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

  1. 两个不同线程,没有锁、没有release/acquire配对、没有join;普通变量一写多读。→ 数据竞争,UB
  2. store(release) + load(relaxed):就算读到release写入的值,不会建立跨线程happens‑before

release只标记输出点;relaxed load不会"捕获"这个先行发生关系。
数据竞争定义:两个不同线程,无happens‑before约束下,同时访问同一个非原子对象,至少一个是写 → UB。


2. happens‑before 对「代码序」、「指令序」的影响

先把三个概念分清:

  1. 代码序(源码序):你写C++文本从上到下的顺序。
  2. happens‑before序:C++抽象模型的逻辑时序。
  3. 指令序(机器指令序):编译器输出汇编,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:

  1. S happens‑before L(配对产生跨线程h‑b)
  2. W happens‑before S(生产者线程内程序顺序)
  3. 传递性:W happens‑before R

语义效果:

  1. W的写对R可见;
  2. 约束: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 到底允许/禁止哪些重排?

把约束翻译成对指令序的规则:

  1. 如果A happens‑before B:
    • 逻辑模型上A在B前面;
    • 机器指令可以乱序,只要从所有线程的观察视角,不表现出"B完成而A未完成"
  2. 线程内:只要本线程看不出变化,指令随便调。
  3. 跨线程有h‑b约束:不能出现"后发生的操作已经对外可见,先发生的操作还不可见"。

happens‑before 不是禁止一切重排;禁止的是"对外暴露破坏逻辑时序的现象"

2.5 seq_cst 的特殊情况

memory_order_seq_cst 除了拥有release/acquire语义,额外增加:

全部seq‑cst原子操作存在一个全局总序 ,所有线程看到同一套顺序。

这会进一步限制指令重排,x86会生成mfence/锁前缀;ARM会生成较重dmb屏障。

release‑acquire 没有全局总序,只约束配对的两个线程,其他线程可以看到不同操作顺序。


核心总结(便于记忆)

  1. happens‑before是逻辑时序,不是机器指令时序。

    不等于汇编指令顺序,不等于CPU执行顺序。

  2. 线程内:源码顺序直接给出happens‑before;机器指令允许重排,但不能改变本线程可观测结果。不需要内存屏障。

  3. 线程内的happens‑before不会自动跨线程传播。

    需要锁 / release‑acquire / join 等手段建立跨线程h‑b链条。

  4. memory_order_release

    约束:本线程前面内存访问不能重排到store之后;创造一个可以被acquire捕获的同步出口。

  5. memory_order_acquire

    约束:本线程后面内存访问不能重排到load之前;捕获对方release,建立跨线程happens‑before链条。

  6. 只要缺少跨线程happens‑before,哪怕机器没有发生任何指令重排,仅仅缓存/StoreBuffer可见性延迟,依然属于C++标准下的数据竞争UB。

通俗比喻:

happens‑before 是法律条文(C++标准);

编译器、CPU是现实执行机构,可以内部优化(重排指令),但对外行为不能违反法律条文

如果你需要,我可以给一张极简示意图:展示「程序顺序 → happens‑before链条 → 对汇编指令的约束」。

相关推荐
zmzb010332 分钟前
C++课后习题训练记录Day201
c++·算法·图论
Escalating_xu33 分钟前
【C++入门基础(上)】从发展历程、学习路线到命名空间与输入输出
开发语言·c++
jimy136 分钟前
C++ 中的rValue的判断---是否能“隐式移动”
开发语言·c++
五_谷_丰_登42 分钟前
平衡二叉搜索树讲解
数据结构·c++
星星.7221 小时前
【图论】最小生成树|Prim+Kruskal算法
数据结构·c++·算法·图论
charlie1145141911 小时前
Cinux · musl 静态移植:对齐 Linux ABI、铺初始栈,以及一个被 SMAP 拦下的潜伏 bug
linux·开发语言·c++·操作系统·开源项目
_Narcissus_1 小时前
单调栈笔记及例题详解
数据结构·c++·笔记·算法·力扣·单调栈·洛谷
流浪0011 小时前
C/C++后端筑基系列(二):C++ 从基础到进阶
开发语言·c++