一、引言:為什麼需要 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 可見且有序 |
定義:
若事件 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)。
例如:
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。示意如下:
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 返回 | 重量 | 隱含 acquire | join 後自動同步,語義清晰,但涉及線程結束與系統調用的較大開銷,掌握原理即可 |
💡 工程經驗總結
- 輕量級同步:線程間數據傳遞或 lock-free 數據結構,首選 atomic + release/acquire 或 acq_rel,必要時用 seq_cst 提供全局順序保證。
- 重量級同步:如果 lock-free 實現難以實現或共享數據結構複雜且性能要求不高,就用 std::mutex、std::condition_variable,簡單安全、語義清晰。
八、延伸閲讀
- C++ 標準草案 §6.9.2
- cppreference: Memory Order
九、總結
在單線程中,代碼的執行順序看似簡單;但在多線程中,編譯器優化和 CPU 亂序會讓順序變得不可預測。
happens-before 正是定義“哪些操作結果必須對其他線程可見”的關鍵語義。
理解 happens-before,能讓你真正掌握:
- 為什麼 release-acquire 能保證可見性;
- 為什麼一個簡單的標誌位,必須用 std::atomic?
- 怎麼寫多線程程序?
對於併發開發者來説,這不只是理論概念,而是編寫正確、高性能多線程程序的根基。
能用鎖寫代碼的人很多,但真正理解 happens-before 的人,才能寫出可預測、可靠的、高性能的併發系統。
happens-before,是 C++ 併發世界的物理定律。理解它,標誌着你真正踏入了 C++ 工程師的高階領域。
📬 歡迎關注公眾號“Hankin-Liu的技術研究室”,收徒傳道。持續分享信創、軟件性能測試、調優、編程技巧、軟件調試技巧相關內容,輸出有價值、有沉澱的技術乾貨。