一、引言:为什么需要 happens-before?

在多线程程序中,“语句顺序” ≠ “执行顺序”。
现代 CPU 和编译器会对指令重排,只要单线程的结果不变,就可以自由优化。
然而,在并发场景下,这会导致严重的问题:

bool ready = false;
int data = 0;

void writer() {
    data = 42;
    ready = true;
}
void reader() {
    while (!ready) ; // 忙等
    std::cout << data << std::endl;
}

你可能以为这段代码一定打印 42,但实际上可能输出 0。
原因是:编译器可能把 ready = true 提前执行,或者 CPU 写缓存在还没同步前被另一个线程读取。
为了定义什么是“可见的先后顺序”,C++ 引入了 happens-before 语义。

二、happens-before 的核心定义

在 C++ 内存模型中,有三个核心关系:

名称作用范围含义
sequenced-before同一线程内部程序语句的逻辑先后(编译器可重排,但结果等价)
synchronizes-with跨线程(同步事件)表示跨线程的同步关系,使一个线程中的操作结果在另一线程中可见,并建立明确的执行顺序。
happens-before全局(包含跨线程)A happens-before B 意味着 A 的结果对 B 可见且有序

定义:

官方链接请见本文第八章“八、延伸阅读”。

三、同步关系(synchronizes-with)

C++ 提供的主要跨线程同步手段是 原子操作(std::atomic)。
例如:

std::atomic<bool> ready{false};
int data = 0;

void writer() {
    data = 42;
    ready.store(true, std::memory_order_release);
}
void reader() {
    while (!ready.load(std::memory_order_acquire))
        ;
    std::cout << data << std::endl;
}

工作原理:

  1. writer 中的 store(..., memory_order_release) —— 发布操作。
  2. reader 中的 load(..., memory_order_acquire) —— 获取操作。
  3. 若读取到的值为 true,则:ready.store(release) synchronizes-with ready.load(acquire) 这建立了一个 happens-before 关系:writer 中的数据写入 → reader 中的读取

结果保证:reader 一定看到 data == 42。示意如下:

Thread 1 (writer)             Thread 2 (reader)
------------------            ------------------
data = 42;                    while (!ready) ;
ready.store(true, release);   if (ready.load(acquire))
                                   // data == 42 保证可见

四、内存序模型概览

内存序描述典型场景
memory_order_relaxed无顺序约束,只保证原子性计数器、统计类变量
memory_order_acquire读取时阻止本线程后续操作重排到前面与 release 配合使用 ,线程间数据同步
memory_order_release写入时阻止前序操作重排到后面与 acquire 配合使用,线程间数据同步
memory_order_acq_rel同时具备 acquire + release 效果原子读-修改-写(RMW, Read-Modify-Write)操作
memory_order_seq_cst最强顺序保证,全局总序默认语义

五、示例:release/acquire 建立的 happens-before

#include <atomic>
#include <thread>
#include <iostream>

std::atomic<int> flag{0};
int data = 0;

void writer() {
    data = 100;
    flag.store(1, std::memory_order_release);
}

void reader() {
    while (flag.load(std::memory_order_acquire) != 1)
        ;
    std::cout << "data = " << data << std::endl;
}

int main() {
    std::thread t1(writer);
    std::thread t2(reader);
    t1.join();
    t2.join();
}

输出:

data = 100

保证不会输出 0。
因为:flag.store(release) synchronizes-with flag.load(acquire) → data 的写入对 reader 可见。

六、错误示例:缺乏 happens-before 的数据竞争

#include <thread>
#include <iostream>

bool ready = false;
int data = 0;

void writer() {
    data = 42;
    ready = true; // 普通变量,没有release语义
}

void reader() {
    while (!ready) ;
    std::cout << data << std::endl; // 可能输出 0!
}

这里没有任何 synchronizes-with 关系,reader 线程的读取结果未定义(UB),可能输出0。

七、工程师视角:happens-before 实战经验总结

在多线程系统中,happens-before 不是抽象理论,而是工程师判断“数据是否可见”的实战准绳。
掌握它,你就能判断代码中是否存在竞态条件,也能避免无谓的锁和性能浪费。

多线程 happens-before 实战总结

手段类型内存序 / 特性性能 / 工程建议
std::atomic(release → acquire)或 atomic_thread_fence(release/acquire)轻量release / acquire性能优于锁,常用于线程间通信或 lock-free 数据结构中。
std::atomic(memory_order_acq_rel,read-modify-write)轻量acq_rel性能优于锁,保证读前可见性 + 写后可见性,适合 lock-free 算法
std::atomic(memory_order_seq_cst,严格顺序)轻量seq_cst性能优于锁,提供全局顺序保证,适合复杂 lock-free 算法
std::mutex:unlock → lock重量隐含 acquire-release系统调用级开销,开销较高,安全可靠,语义清晰,适合强一致性、强调安全性与可维护性而非极致性能场景
std::condition_variable:notify → wait 返回重量需配合锁使用,隐含 acquire-release系统调用开销大,不宜用于低延迟关键路径,适用于线程需等待特定条件或事件的场景(如生产者-消费者队列),能提供可靠同步。
std::thread:启动 → 线程体执行重量隐含 release系统调用开销大,掌握原理即可
std::thread::join():线程结束 → join 返回重量隐含 acquirejoin 后自动同步,语义清晰,但涉及线程结束与系统调用的较大开销,掌握原理即可

工程经验总结

  • 轻量级同步:线程间数据传递或 lock-free 数据结构,首选 atomic + release/acquire 或 acq_rel,必要时用 seq_cst 提供全局顺序保证。
  • 重量级同步:如果 lock-free 实现难以实现或共享数据结构复杂且性能要求不高,就用 std::mutex、std::condition_variable,简单安全、语义清晰。

八、延伸阅读

  1. C++ 标准草案 §6.9.2
  2. cppreference: Memory Order

九、总结

在单线程中,代码的执行顺序看似简单;但在多线程中,编译器优化和 CPU 乱序会让顺序变得不可预测。
happens-before 正是定义“哪些操作结果必须对其他线程可见”的关键语义。 理解 happens-before,能让你真正掌握:

  • 为什么 release-acquire 能保证可见性;
  • 为什么一个简单的标志位,必须用 std::atomic?
  • 怎么写多线程程序?

对于并发开发者来说,这不只是理论概念,而是编写正确、高性能多线程程序的根基。
能用锁写代码的人很多,但真正理解 happens-before 的人,才能写出可预测、可靠的、高性能的并发系统。
happens-before,是 C++ 并发世界的物理定律。理解它,标志着你真正踏入了 C++ 工程师的高阶领域。

欢迎关注公众号“Hankin-Liu的技术研究室”,收徒传道。持续分享信创、软件性能测试、调优、编程技巧、软件调试技巧相关内容,输出有价值、有沉淀的技术干货。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]