Item 36:若非同步執行不可或缺,就指定 std::launch::async (Specify std::launch::async if Asynchronicity Is Essential)
Overview Table
| 重點 | 說明 |
|---|---|
| 兩種 launch policy | std::launch::async(必須在另一條 thread 非同步執行)與 std::launch::deferred(延遲到對 future 呼叫 get/wait 時同步執行) |
| 預設政策 | 兩者 OR 起來:std::launch::async | std::launch::deferred,即由執行期系統自行決定非同步或延遲 |
| 預設政策的代價 | 無法預測是否併發執行、在哪條 thread 執行、甚至是否執行;thread_local 變數歸屬不確定;wait_for/wait_until 迴圈可能永不終止 |
| 修正手法 | 先以 fut.wait_for(0s) 檢查是否回傳 std::future_status::deferred,再決定走同步路徑或 timeout 迴圈 |
| 核心結論 | 若任務必須真正非同步執行,呼叫時明確傳入 std::launch::async(或使用自製的 reallyAsync) |
兩種 Launch Policy 與「出乎意料」的預設值
std::async 並不是「請幫我非同步執行 f」,而是「請依 launch policy 執行 f」。policy 是 std::launch 這個 scoped enum(見 Item 10)的列舉值:
| Policy | 語意 | 執行時機 / 執行緒 |
|---|---|---|
std::launch::async |
f 必須非同步執行 | 立即排程於不同 thread |
std::launch::deferred |
f 延遲執行 | 僅當對 future 呼叫 get 或 wait 時,於呼叫者 thread 同步執行(呼叫者阻塞至 f 結束);若從未呼叫 get/wait,f 永遠不會執行 |
auto fut1 = std::async(f); // 使用「預設」launch policy
auto fut2 = std::async(std::launch::async | // 與 fut1 完全等價:
std::launch::deferred, // async「或」deferred,
f); // 由系統挑選
Important
預設政策 = async | deferred。這個彈性讓標準函式庫得以接手 thread 建立/銷毀、避免 oversubscription、負載平衡(Item 35 的優點來源),但也帶來下述不確定性。
預設政策的三大不確定性
設 thread t 執行 auto fut = std::async(f);:
- 無法預測 f 是否與 t 併發執行——f 可能被排為 deferred。
- 無法預測 f 是否跑在與呼叫
get/wait的 thread 不同的 thread 上。 - 可能無法預測 f 是否會執行——若某些程式路徑不會對
fut呼叫get/wait,deferred 的 f 就永遠不執行。
連帶影響:
thread_local變數(TLS):f 讀寫的 thread-local storage 可能屬於獨立 thread,也可能屬於呼叫get/wait的 thread——無法預測。- timeout 型 wait 迴圈:對 deferred 任務呼叫
wait_for/wait_until會回傳std::future_status::deferred,永遠不會是ready:
using namespace std::literals; // C++14 duration 字尾
void f() { std::this_thread::sleep_for(1s); } // f 睡 1 秒後返回
auto fut = std::async(f); // 「概念上」非同步執行 f
while (fut.wait_for(100ms) != // 等 f 結束……
std::future_status::ready) // 若 f 被 deferred,
{ /* ... */ } // 迴圈永不終止!
預設政策下 wait_for 的兩種世界:
┌── policy 實為 async ────► wait_for 最終回傳 ready ──► 迴圈結束
std::async(f)─┤
└── policy 實為 deferred ─► wait_for 永遠回傳 deferred ► 無窮迴圈
Danger
這種 bug 在開發與單元測試時極易漏掉:只有在高負載(機器接近 oversubscription 或 thread exhaustion)時,任務才最可能被 deferred,問題才浮現。
修正:以 wait_for(0s) 偵測 deferred
future 沒有「是否 deferred」的直接查詢 API,只能借用 timeout 函式、傳入 0 逾時來探測:
auto fut = std::async(f); // 同前
if (fut.wait_for(0s) == // 任務被 deferred?
std::future_status::deferred)
{
/* 對 fut 用 wait 或 get 同步呼叫 f */
} else { // 任務不是 deferred
while (fut.wait_for(100ms) != // 不可能無窮迴圈
std::future_status::ready) // (假設 f 會結束)
{
/* 任務尚未 ready,先做其他併發工作 */
}
/* fut 已 ready */
}
預設政策的適用檢查表——以下全部成立時,用預設政策才安全:
| # | 條件 |
|---|---|
| 1 | 任務不需要與呼叫 get/wait 的 thread 併發執行 |
| 2 | 讀寫哪條 thread 的 thread_local 變數無關緊要 |
| 3 | 保證會對 future 呼叫 get/wait,或可接受任務永不執行 |
| 4 | 使用 wait_for/wait_until 的程式碼已考慮 deferred 狀態 |
任一條不成立 → 明確指定 std::launch::async:
auto fut = std::asyncasync, f; // 保證真正非同步執行 f
Warning
「對 std::async 回傳的 future 呼叫 get/wait」是簡化說法:真正關鍵的是 future 所指涉的 shared state(見 Item 38)。future 可被 move、也可轉成可複製的 std::shared_future,因此實際觸發執行的 future 物件未必是 std::async 直接回傳的那一個。
自製 reallyAsync:永遠使用 async 政策
// C++11:以 std::result_of 推出回傳型別(type traits 見 Item 9)
template<typename F, typename... Ts>
inline
std::future<typename std::result_of<F(Ts...)>::type>
reallyAsync(F&& f, Ts&&... params) // 回傳非同步呼叫
{ // f(params...) 的 future
return std::async(std::launch::async,
std::forward<F>(f), // 完美轉發(見 Item 25)
std::forward<Ts>(params)...);
}
// C++14:auto 回傳型別推導,更精簡
template<typename F, typename... Ts>
inline
auto reallyAsync(F&& f, Ts&&... params)
{
return std::async(std::launch::async,
std::forward<F>(f),
std::forward<Ts>(params)...);
}
auto fut = reallyAsync(f); // 非同步執行 f;若 std::async
// 無法建立 thread 則丟出例外
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
std::async(f) 的預設 launch policy 是什麼? |
std::launch::async | std::launch::deferred(兩者 OR,由系統決定) |
std::launch::deferred 的任務何時執行? |
對 future 呼叫 get/wait 時,在呼叫者 thread 同步執行;從未呼叫則永不執行 |
while (fut.wait_for(100ms) != ready) 永不終止 |
任務被 deferred,wait_for 永遠回傳 std::future_status::deferred |
| 如何檢查任務是否被 deferred? | 呼叫 fut.wait_for(0s) 並比對 std::future_status::deferred(無直接查詢 API) |
| bug 只在高負載/正式環境出現、測試抓不到 | oversubscription / thread exhaustion 時任務才容易被 deferred |
f 使用 thread_local 變數 + 預設政策 |
無法預測存取哪條 thread 的 TLS → 應指定 std::launch::async |
| 「任務必須真正併發執行」 | 傳入 std::launch::async 作第一個引數,或用 reallyAsync 包裝 |
reallyAsync 的實作要點 |
完美轉發 f 與參數給 std::async,固定 std::launch::async;C++11 用 std::result_of 寫回傳型別、C++14 用 auto |
Related Notes
- 08-Concurrency-API/01-Task-Based-Vs-Thread-Based — Item 35:預設政策的彈性正是 task-based 程式設計能自動處理 oversubscription/負載平衡的原因
- 08-Concurrency-API/04-Thread-Handle-Destructor-Behavior — Item 38:future 與 shared state 的關係,及
std::async產生之 future 的特殊解構行為 - 08-Concurrency-API/05-Void-Futures-For-Events — Item 39:以 future 進行事件通訊的替代設計
- 04-Moving-To-Modern-Cpp/04-Scoped-Enums — Item 10:
std::launch即 scoped enum - 06-Move-Semantics-And-Perfect-Forwarding/03-Move-Vs-Forward-Usage — Item 25:
reallyAsync對 universal reference 參數使用std::forward - 08-Concurrency-API/Practice-Concurrency-API — 本章練習題