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 的關鍵優勢:
- 回傳值:
std::threadAPI 沒有直接管道取得非同步函式的回傳值;future 的get()有。 - 例外:thread-based 下函式丟例外,程式經
std::terminate死亡;task-based 下get()能存取該例外。 - 抽象層級更高:把 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 要結果的那條執行緒」(呼叫 get 或 wait 者)上執行。系統超載時,合理的排程器就會利用這個彈性。
例外與代價
- 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、affinity;std::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 不強制 |
Related Notes
- 08-Concurrency-API/02-Std-Launch-Async — 預設 launch policy 的彈性與陷阱;
std::launch::async何時必須指定 - 08-Concurrency-API/03-Threads-Unjoinable-On-All-Paths — 直接用
std::thread時必須處理的 joinability 問題(正是 task-based 幫你省掉的負擔) - 08-Concurrency-API/04-Thread-Handle-Destructor-Behavior —
std::thread與 future 同為 thread handle,解構行為卻大不相同 - 04-Moving-To-Modern-Cpp/08-Noexcept —
noexcept函式仍無法阻止建立執行緒時的std::system_error - 08-Concurrency-API/Practice-Concurrency-API — 本章練習題