Item 37:讓 std::thread 在所有路徑上皆為 unjoinable (Make std::threads unjoinable on all paths)

Overview Table

重點 說明
核心規則 std::thread 離開 scope 的每一條路徑上都必須是 unjoinable,否則程式直接終止
joinable 解構的後果 解構 joinable 的 std::threadstd::terminate(標準委員會刻意的設計)
implicit join 的問題 若解構子隱式 join → 難以除錯的效能異常(函式莫名等待背景工作完成)
implicit detach 的問題 若解構子隱式 detach → 難以除錯的未定義行為(背景執行緒寫入已被回收的 stack 記憶體)
標準解法 自行撰寫 RAII 類別(如 ThreadRAII),在解構子中依指定動作 joindetach
成員宣告順序 std::thread 資料成員應宣告在最後——建構後可能立即執行,須確保其他成員已初始化

Joinable 與 Unjoinable 狀態

Joinablestd::thread 對應到一條底層的非同步執行緒(執行中、被阻塞、等待排程、甚至已跑完都算 joinable)。

Unjoinable 的四種情況:

情況 原因
預設建構std::thread 沒有函式可執行,不對應任何底層執行緒
被 move 走std::thread 底層執行緒的對應關係已轉移給另一個 std::thread
已 joinstd::thread join 之後不再對應已結束的底層執行緒
已 detachstd::thread detach 切斷了與底層執行緒的連結
                 ┌────────────────────┐
  std::thread t(func) ──▶ │      joinable      │ ──解構子被呼叫──▶ std::terminate!
                 └────────┬───────────┘                  (程式終止)
                          │
        join() / detach() / 被 move 走
                          │
                          ▼
                 ┌────────────────────┐
                 │     unjoinable     │ ──解構子被呼叫──▶ 安全,正常解構
                 └────────────────────┘
Danger

「所有路徑」包含:正常流到 scope 結尾、returncontinuebreakgoto丟出例外。任何一條路徑漏掉,joinable thread 的解構就會終止整個程式。

為何解構 joinable thread 要終止程式?

doWork 為例:lambda 以 reference 捕獲區域變數 goodVals 並在背景執行緒中 push_back;若 conditionsAreSatisfied() 回傳 false 或丟出例外,t 在解構時仍為 joinable:

constexpr auto tenMillion = 10'000'000;          // C++14 可用 ' 作數字分隔符

bool doWorkfunction<bool(int)> filter, int maxVal = tenMillion
{
  std::vector<int> goodVals;                     // 通過 filter 的值

  std::thread t([&filter, maxVal, &goodVals]     // 背景執行緒填入 goodVals
                {
                  for (auto i = 0; i <= maxVal; ++i)
                    { if (filter(i)) goodVals.push_back(i); }
                });

  auto nh = t.native_handle();                   // 用 native handle 設定優先權
  // ...                                         //(futures API 拿不到,故用 thread)

  if (conditionsAreSatisfied()) {
    t.join();                                    // 正常路徑:join 後 unjoinable
    performComputation(goodVals);
    return true;
  }
  return false;   // 問題路徑!t 仍 joinable → 解構 → 程式終止
}

委員會權衡過另外兩個選項,結論是都更糟

解構子預設行為 後果
implicit join doWork 明明已判定不需計算,卻仍等 filter 跑完千萬筆——反直覺的效能異常,極難追查
implicit detach doWork 返回後 stack frame 被回收,背景 lambda 繼續對「曾是 goodVals」的記憶體 push_back;後續函式 f 的 stack 內容憑空改變——除錯噩夢級的未定義行為
終止程式(實際採用) 後果明確、立即可見,被認為是「爛選項中最不爛的」

ThreadRAII:用 RAII 保證所有路徑 unjoinable

要在「離開 block 的每條路徑」執行同一動作,標準手法就是放進區域物件的解構子(RAII)。標準庫沒有為 std::thread 提供 RAII 類別,需自己寫:

class ThreadRAII {
public:
  enum class DtorAction { join, detach };        // scoped enum,見 Item 10

  ThreadRAIIthread&& t, DtorAction a      // 只收 rvalue:thread 不可複製,
  : action(a), tmove(t) {}                // 必須 move 進來

  ~ThreadRAII()
  {
    if (t.joinable()) {                          // 必要檢查!對 unjoinable thread
      if (action == DtorAction::join) {          // 呼叫 join/detach 是 UB
        t.join();
      } else {
        t.detach();
      }
    }
  }

  ThreadRAII(ThreadRAII&&) = default;            // 宣告了解構子會抑制 move 生成,
  ThreadRAII& operator=(ThreadRAII&&) = default; // 故顯式 =default(見 Item 17)

  std::thread& get() { return t; }               // 仿智慧指標的 get(),
                                                 // 免去複製整套 thread 介面
private:
  DtorAction action;
  std::thread t;                                 // 刻意宣告在最後!
};

設計要點:

Warning

「解構子中 joinable() 檢查與 join()/detach() 之間有 race」是錯誤直覺std::thread 只能透過成員函式呼叫(join、detach、move)改變 joinability。若解構子執行時真有其他執行緒同時呼叫 t 的成員函式,race 存在於呼叫端程式碼(同時對同一物件呼叫兩個成員函式),而非 ThreadRAII 內部——同一物件的並行成員函式呼叫只有在全部是 const 成員函式時才安全(見 Item 16)。

Warning

選擇 DtorAction::join 只是「爛選項中最好的」:Item 39 展示 join-on-destruction 不只可能造成效能異常,甚至可能程式懸住 (hang)。「正確」解法是通知非同步 lambda 提前返回,但 C++11 沒有 interruptible threads(可手工實作,超出本書範圍)。

Exam/Test Patterns

情境關鍵字 答案
解構 joinablestd::thread 會怎樣? 程式終止std::terminate),不是 UB、不是隱式 join/detach
哪些 std::threadunjoinable 預設建構、被 move 走、已 join、已 detach
已跑完的底層執行緒,其 std::thread 是否 joinable? ——run to completion 仍算 joinable,解構前仍須 join
為何標準不採 implicit join 難以除錯的效能異常(明明不需要結果卻苦等背景工作)
為何標準不採 implicit detach 難以除錯的未定義行為(背景執行緒寫入已回收的 stack frame)
如何保證所有離開路徑(return / break / 例外…)都 unjoinable? RAII 物件(如 ThreadRAII),把動作放在解構子
unjoinable thread 呼叫 join()/detach() 未定義行為——所以 ThreadRAII 解構子必先測 t.joinable()
std::thread 資料成員應宣告在哪? 類別成員列表最後(建構後可能立即執行,須確保其他成員已初始化)
ThreadRAII 宣告了解構子,move 操作怎麼辦? 解構子抑制編譯器生成 move,需 =default 顯式要回(Item 17)
為何不用 task(std::async)而用 std::thread 需要 native_handle() 設定優先權等——futures API 不提供