一、引言:为什么需要 happens-before?
在多线程程序中,"语句顺序" ≠ "执行顺序"。
现代 CPU 和编译器会对指令重排,只要单线程的结果不变,就可以自由优化。
然而,在并发场景下,这会导致严重的问题:
cpp
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 可见且有序 |
定义:
若事件 A sequenced-before B,则 A happens-before B(线程内)。
若 A synchronizes-with B,则 A happens-before B(跨线程)。
若 A happens-before B,且 B happens-before C,则 A happens-before C(可传递)。
官方链接请见本文第八章"八、延伸阅读"。
三、同步关系(synchronizes-with)
C++ 提供的主要跨线程同步手段是 原子操作(std::atomic)。
例如:
cpp
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;
}
工作原理:
- writer 中的 store(..., memory_order_release) ------ 发布操作。
- reader 中的 load(..., memory_order_acquire) ------ 获取操作。
- 若读取到的值为 true,则:ready.store(release) synchronizes-with ready.load(acquire) 这建立了一个 happens-before 关系:writer 中的数据写入 → reader 中的读取
结果保证:reader 一定看到 data == 42。示意如下:
cpp
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
cpp
#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();
}
输出:
cpp
data = 100
保证不会输出 0。
因为:flag.store(release) synchronizes-with flag.load(acquire) → data 的写入对 reader 可见。
六、错误示例:缺乏 happens-before 的数据竞争
cpp
#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 返回 | 重量 | 隐含 acquire | join 后自动同步,语义清晰,但涉及线程结束与系统调用的较大开销,掌握原理即可 |
💡 工程经验总结
- 轻量级同步:线程间数据传递或 lock-free 数据结构,首选 atomic + release/acquire 或 acq_rel,必要时用 seq_cst 提供全局顺序保证。
- 重量级同步:如果 lock-free 实现难以实现或共享数据结构复杂且性能要求不高,就用 std::mutex、std::condition_variable,简单安全、语义清晰。
八、延伸阅读
九、总结
在单线程中,代码的执行顺序看似简单;但在多线程中,编译器优化和 CPU 乱序会让顺序变得不可预测。
happens-before 正是定义"哪些操作结果必须对其他线程可见"的关键语义。 理解 happens-before,能让你真正掌握:
- 为什么 release-acquire 能保证可见性;
- 为什么一个简单的标志位,必须用 std::atomic?
- 怎么写多线程程序?
对于并发开发者来说,这不只是理论概念,而是编写正确、高性能多线程程序的根基。
能用锁写代码的人很多,但真正理解 happens-before 的人,才能写出可预测、可靠的、高性能的并发系统。
happens-before,是 C++ 并发世界的物理定律。理解它,标志着你真正踏入了 C++ 工程师的高阶领域。
📬 欢迎关注公众号"Hankin-Liu的技术研究室",收徒传道。持续分享信创、软件性能测试、调优、编程技巧、软件调试技巧相关内容,输出有价值、有沉淀的技术干货。