Item 35:偏好 task-based 而非 thread-based 程式設計 (Prefer Task-Based Programming to Thread-Based)

Overview Table

比較面向 Thread-based(std::thread Task-based(std::async
取得回傳值 無直接管道 future 的 get() 直接取得
例外處理 函式丟出例外 → std::terminate 程式終止 get() 可取得並重新拋出例外
thread 耗盡(exhaustion) 建立失敗丟 std::system_error,需自行處理 責任轉移給 Standard Library 實作者,幾乎不會發生
oversubscription(超額訂閱) 自行管理 排程器可延遲執行、彈性調度
load balancing(負載平衡) 自行管理 runtime 排程器掌握全機資訊,做得更好
抽象層級 低階:直接操作執行緒 高階:只描述「要做什麼」,不管「在哪條執行緒做」
結論 少數特殊情境才用 預設首選

兩種非同步寫法:std::thread vs std::async

int doAsyncWork();                    // 要非同步執行的函式

// Thread-based:建立 std::thread 執行
std::thread t(doAsyncWork);           // 拿不到回傳值;
                                      // 若 doAsyncWork 丟例外 → std::terminate

// Task-based:把函式當「task」交給 std::async
auto fut = std::async(doAsyncWork);   // "fut" 即 future
auto result = fut.get();              // 輕鬆取得回傳值;
                                      // 例外也經由 get() 傳遞,可 catch

傳給 std::async 的函式物件稱為 task。task-based 的關鍵優勢:

  1. 回傳值std::thread API 沒有直接管道取得非同步函式的回傳值;future 的 get() 有。
  2. 例外:thread-based 下函式丟例外,程式經 std::terminate 死亡;task-based 下 get() 能存取該例外。
  3. 抽象層級更高:把 thread 管理細節(建立、排程、負載)整批丟給 Standard Library 實作者。

「Thread」在 C++ 並行程式中的三種意義

種類 說明
Hardware threads 真正執行運算的執行緒;每個 CPU core 提供一或多個
Software threads(OS threads / system threads) OS 跨所有 process 管理、排程到 hardware thread 上執行;可多於 hardware thread(有人 blocked 時換別人跑以提升吞吐量)
std::thread C++ process 內的物件,作為底層 software thread 的 handle;可能是「null handle」
 C++ 程式          作業系統               硬體
+-------------+   +-----------------+   +------------------+
| std::thread |-->| software thread |-->| hardware thread  |
|  (handle)   |   |  (OS 排程單位)   |   | (CPU core 執行)  |
+-------------+   +-----------------+   +------------------+
   可能為 null        數量有限、可超額         每 core 1+ 個

std::thread 成為「null handle」(不對應任何 software thread)的四種情況:default-constructed(沒有函式可執行)、被 move 走已 join(函式跑完了)、已 detach(與底層執行緒斷開連結)。

Thread-based 要自己扛的三大麻煩

麻煩 內容 後果
Thread exhaustion(執行緒耗盡) software thread 是有限資源,要求超過系統上限 丟出 std::system_error——即使該函式是 noexcept 也一樣(例外來自建立執行緒,不是函式本身)
Oversubscription(超額訂閱) ready-to-run 的 software threads 多於 hardware threads 排程器 time-slicing → context switch 成本;換 core 時 CPU cache 變冷、又污染別人的 cache
Load balancing(負載平衡) 軟硬體執行緒最佳比例隨程式階段(I/O-heavy vs computation-heavy)與機器架構動態變化 在某平台調校好也無法保證移植到其他機器仍有效
int doAsyncWork() noexcept;        // noexcept 見 Item 14

std::thread t(doAsyncWork);        // 若無執行緒可用,仍會丟出例外!

auto fut = std::async(doAsyncWork); // thread 管理責任交給
                                    // Standard Library 實作者

std::async(使用預設 launch policy)之所以能大幅減少 out-of-threads 例外:它不保證建立新的 software thread,而是允許排程器把函式安排在「向 fut 要結果的那條執行緒」(呼叫 getwait 者)上執行。系統超載時,合理的排程器就會利用這個彈性。

例外與代價

  • GUI 執行緒回應性:排程器不知道哪條執行緒有即時回應需求,可能把 task 排到 GUI 執行緒上執行。此時應改傳 std::launch::async,強制在不同執行緒執行(見 Item 36)。
  • 預設 policy 的不確定性:task 可能被 deferred 而永不執行、wait_for 迴圈可能無限循環等問題,詳見 Item 36。
  • 負載平衡問題並未消失,只是交給「掌握全機所有 process 資訊」的 runtime 排程器處理,通常比你自己處理得好。

最先進的排程器使用 system-wide thread pool 避免 oversubscription,並以 work-stealing 演算法改善跨 core 負載平衡。C++ Standard 不強制要求這些技術,但使用 task-based 設計,未來這些技術普及時就能自動受益;直接用 std::thread 則得自己扛下所有問題。

少數適合直接使用 std::thread 的情境

情境 原因
需要底層 threading API 平台 API(pthreads、Windows Threads)比 C++ 豐富,如 thread priority、affinitystd::thread 提供 native_handle(),而 std::future 沒有對應功能
有能力且有必要為應用最佳化 thread 用量 如:執行剖面已知的 server 軟體,部署在硬體固定、且它是機器上唯一重要 process 的環境
需實作 C++ API 之外的 threading 技術 如:在沒有 thread pool 的平台上自己實作 thread pool
這些都是不常見的情況。絕大多數時候,應選擇 task-based 設計,而不是直接操作 thread。

Exam/Test Patterns

情境關鍵字 答案
非同步函式的「回傳值」怎麼拿? std::async 回傳的 future 呼叫 get()std::thread 沒有直接管道
std::thread 執行的函式丟出例外 程式呼叫 std::terminate 終止;task-based 則由 get() 傳遞例外
noexcept 函式 + std::thread 建構 仍可能丟 std::system_error(software threads 耗盡)
「ready-to-run threads > hardware threads」 oversubscription:context switch 開銷、cache 變冷/污染
為何 std::async 較不會 out-of-threads? 預設 launch policy 不保證開新執行緒,可安排在呼叫 get/wait 的執行緒上執行
GUI 執行緒需要即時回應 std::launch::async 強制在不同執行緒執行(Item 36)
「thread」的三種意義 hardware threads / software (OS) threads / std::thread(handle,可能為 null)
何時直接用 std::thread 需要 native_handle()(priority/affinity)、能自行最佳化 thread 用量、需自建 thread pool 等技術
thread pool、work-stealing 先進排程器的技術;task-based 設計可自動受益,Standard 不強制