記憶體模型基礎 (Memory Model Basics)

Overview Table

概念 重點
物件與記憶體位置 每個物件至少佔一個記憶體位置 (memory location);相鄰位元欄位共享同一位置
併發與記憶體位置 兩執行緒無序存取同一位置且至少一個寫入 → 資料競爭 (data race)未定義行為
避免資料競爭 互斥鎖 (mutex) 排序,或用原子操作排序
修改順序 (modification order) 每個物件有一條全體執行緒必須認可的寫入順序

物件與記憶體位置

C++ 程式中的資料都由物件 (object) 構成,標準將物件定義為「儲存區域」。每個物件無論型別為何,都儲存在一個或多個記憶體位置上;每個記憶體位置要麼是一個標量型別 (scalar type) 的物件/子物件,要麼是一串相鄰位元欄位 (bit-field)

四個必記原則:

  1. 每個變數都是物件,其成員變數也是物件。
  2. 每個物件至少佔一個記憶體位置。
  3. 基本型別(如 intmy_class*)無論大小,都有專屬的記憶體位置(即使相鄰或屬於陣列的一部分)。
  4. 相鄰位元欄位屬於同一個記憶體位置
struct my_data {          記憶體位置分解(對應書中圖 5.1)
  int i;            ──→  位置 1:i
  double d;         ──→  位置 2:d
  unsigned bf1:10;  ─┐
  int bf2:25;       ─┴→  位置 3:bf1 與 bf2(相鄰位元欄位共享一個位置)
  int :0;                (寬度 0 的未命名位元欄位:強制下一欄位對齊)
  int bf4:9;        ──→  位置 4:bf4(因寬度 0 欄位而與 bf1/bf2 分離)
  int i2;           ──→  位置 5:i2
  char c1, c2;      ──→  位置 6:c1;位置 7:c2(各自獨立)
  std::string s;    ──→  位置 8+:s 內部由多個記憶體位置組成
};
寬度 0 位元欄位

寬度為 0 的未命名位元欄位會強制下一個位元欄位對齊到下一個型別邊界,因此把位元欄位序列「切開」成兩個記憶體位置。這是讓兩組位元欄位能被獨立保護(不同 mutex)的手段。

併發與記憶體位置

避免資料競爭的兩種手段:

手段 原理
互斥鎖 同一 mutex 先鎖者先行,同一時間只有一個執行緒能存取該位置
原子操作 對同一位置的存取被賦予明確順序,把行為拉回「有定義」的範圍
未定義行為是 C++ 的黑洞

一旦程式含有任何未定義行為,整個程式的行為都無法預測。資料競爭是嚴重錯誤,必須不惜一切代價避免。

原子操作不會消除競爭本身

原子操作只保證每次存取不可分割、行為有定義;哪個執行緒先存取(競爭的結果)仍未指定。要控制「誰先誰後」仍需記憶體順序與同步(見 05-Memory-Model-and-Atomics/04-Memory-Ordering-Options)。

修改順序 (Modification Order)

每個 C++ 物件從初始化開始,就有一條由程式中所有執行緒共同認可的寫入序列,稱為修改順序


Exam/Test Patterns

情境/關鍵字 答案
兩執行緒同時寫同一記憶體位置、無同步 資料競爭 → 未定義行為
「相鄰位元欄位能否分別用不同 mutex 保護?」 不能,它們是同一個記憶體位置
想把位元欄位切成兩個記憶體位置 中間插入寬度 0 的未命名位元欄位
共享資料全程只讀 不需要保護或同步
「所有執行緒對單一物件的寫入順序」 修改順序,全體執行緒必須一致
「不同物件間的操作順序需要全體一致嗎?」 不需要(除非施加額外順序約束)
原子操作能避免什麼 未定義行為(但不會消除競爭本身)