同步發生與先行發生 (Synchronizes-With and Happens-Before)
Overview Table
| 關係 | 適用範圍 | 一句話定義 |
|---|---|---|
| sequenced-before(序前) | 單一執行緒內 | 源碼中 A 排在 B 之前 → A 序前於 B |
| synchronizes-with(同步發生) | 跨執行緒、只在原子操作間 | 原子讀 R 讀到原子寫 W(或其後續)寫入的值 → W 同步發生於 R |
| inter-thread happens-before(執行緒間先行) | 跨執行緒 | 由同步發生 + 序前組合、可傳遞 |
| happens-before(先行發生) | 全域 | sequenced-before 或 inter-thread happens-before |
| strongly-happens-before(強先行發生) | 全域 | 同上,但排除 memory_order_consume 標記的操作 |
動機場景:以原子旗標排序非原子資料
兩個執行緒:一個填資料、一個讀資料。用 std::atomic<bool> data_ready 當旗標,讀者在旗標設置前不讀資料(代碼 5.2):
std::vector<int> data; // 非原子!
std::atomic<bool> data_ready(false);
void writer_thread() {
data.push_back(42); // ③ 寫資料
data_ready = true; // ④ 設旗標
}
void reader_thread() {
while (!data_ready.load()); // ① 等旗標
std::cout << data[0]; // ② 讀資料
}
writer_thread reader_thread
------------- -------------
③ data.push_back(42)
│ sequenced-before
④ data_ready = true (store)
└────── synchronizes-with ────→ ① data_ready.load() == true
│ sequenced-before
② 讀 data[0]
傳遞得出:③ happens-before ② ⇒ 對非原子 data 的存取沒有資料競爭
- ③ 序前於 ④(同執行緒);④ 的 store 與 ① 讀到
true的 load 同步發生;① 序前於 ②。 - 先行發生可傳遞,故「寫資料 ③ 先行於 讀資料 ②」——原子旗標為非原子操作強制排序。
- 若無此同步,非原子的 ② 與 ③ 對同一資料結構無序存取 → 未定義行為(見 05-Memory-Model-and-Atomics/01-Memory-Model-Basics)。
同步發生 (Synchronizes-With)
只發生在原子型別的操作之間(或內部使用原子操作的機制,如 mutex——見 05-Memory-Model-and-Atomics/05-Release-Sequences-and-Fences)。
基本定義:對變數 x 的一個適當標記的原子寫 W,同步發生於一個適當標記、且讀到下列值之一的原子讀:
- W 本身寫入的值;
- W 之後,同一執行緒對 x 的後續原子寫所寫入的值;
- 任意執行緒對 x 的一連串原子讀-改-寫操作(如
fetch_add()、compare_exchange_weak())所形成序列中的值(→ 釋放序列,詳見 05-Memory-Model-and-Atomics/05-Release-Sequences-and-Fences)。
「適當標記」是什麼?
原子操作預設(memory_order_seq_cst)就是適當標記——所以「執行緒 A 存值、執行緒 B 讀到該值」預設即建立同步發生。所有微妙之處都在其他記憶體順序選項上(見 05-Memory-Model-and-Atomics/04-Memory-Ordering-Options:release store 配 acquire load 才同步;relaxed 不同步)。
先行發生 (Happens-Before) 與 Sequenced-Before
先行發生是程式操作順序的基本建構塊:指定「哪個操作能看見哪個操作的效果」。
- 單執行緒:源碼中操作 A 排在操作 B 之前 → A 序前 (sequenced-before) 於 B,即 A 先行於 B。
- 同一語句內的多個子表達式通常無序:
foo(get_num(), get_num()); // 兩次 get_num() 呼叫順序未指定
// 可能輸出 "1,2" 也可能 "2,1"(代碼 5.3)
「同時發生」通常沒有先行關係
一條語句中的引數求值彼此之間沒有 sequenced-before(逗號運算子、或把表達式結果作為另一表達式的引數等情況除外)。無序 → 無先行發生。
執行緒間先行 (Inter-Thread Happens-Before) 與傳遞性
新意在於跨執行緒的組合規則(依賴同步發生):
| 規則 | 內容 |
|---|---|
| 基礎 | A 同步發生於 B(不同執行緒) → A 執行緒間先行於 B |
| 傳遞 | A 執行緒間先行於 B,且 B 執行緒間先行於 C → A 執行緒間先行於 C |
| 與序前結合 (1) | A 序前於 B,且 B 執行緒間先行於 C → A 執行緒間先行於 C |
| 與序前結合 (2) | A 同步發生於 B,且 B 序前於 C → A 執行緒間先行於 C |
- 實務意義:單執行緒內做一系列修改,只需在最後做一次同步(如設一個旗標),其他執行緒即可看見全部修改——這正是代碼 5.2 能運作的原因。
強先行發生 (Strongly-Happens-Before)
- 大多數情況與先行發生相同:A 同步發生於 B、或 A 序前於 B → A 強先行於 B;同樣可傳遞。
- 差別:被標記為
memory_order_consume的操作參與「執行緒間先行」但不參與「強先行發生」。 - 由於實務上不使用
memory_order_consume(C++17 不建議,見 05-Memory-Model-and-Atomics/04-Memory-Ordering-Options),兩者在多數程式碼中無差別。
Exam/Test Patterns
| 情境/關鍵字 | 答案 |
|---|---|
| 「synchronizes-with 發生在什麼之間?」 | 適當標記的原子操作之間(含原子讀到 RMW 序列中的值) |
| A 序前於 B、B 執行緒間先行於 C | A 執行緒間先行於 C(可與序前結合、可傳遞) |
foo(get_num(), get_num()) 的求值順序 |
未指定,兩引數間無 sequenced-before |
| 「單執行緒內先行發生的基礎」 | sequenced-before(源碼順序) |
| 原子旗標 + 非原子資料的正確性論證 | store/load 同步發生 + 兩端 sequenced-before → 資料寫先行於資料讀 |
| happens-before vs strongly-happens-before | 差在 memory_order_consume(consume 不算強先行) |
| 一串修改後如何讓他執行緒全部看見 | 尾端一次同步即可(先行發生的傳遞性) |
Related Notes
- 05-Memory-Model-and-Atomics/01-Memory-Model-Basics
- 05-Memory-Model-and-Atomics/02-Atomic-Types-and-Operations
- 05-Memory-Model-and-Atomics/04-Memory-Ordering-Options
- 05-Memory-Model-and-Atomics/05-Release-Sequences-and-Fences
- 05-Memory-Model-and-Atomics/Practice-Memory-Model-and-Atomics
- 04-Synchronizing-Operations/01-Condition-Variables-and-Thread-Safe-Queue