Item 38:留意各種 thread handle 的解構子行為 (Be aware of varying thread handle destructor behavior)
Overview Table
| 主題 | 重點 |
|---|---|
| 核心對比 | std::thread 與 future 都是系統執行緒的 handle,但解構子行為完全不同 |
std::thread 解構 |
joinable 時解構 → 程式終止(std::terminate,見 Item 37) |
| future 解構的「正常行為」 | 只銷毀資料成員 + 遞減 shared state 的參考計數;不 join、不 detach、不執行任何東西 |
| future 解構的「唯一例外」 | 是 std::async 建立、launch policy 為 std::launch::async、且為指涉該 shared state 的最後一個 future → 解構子阻塞至任務完成(隱含 join) |
| 結果存放位置 | 存於 caller 與 callee 之外的 shared state(通常是 heap 物件,Standard 未規定實作) |
| API 限制 | 無法從 future 物件查出它是否來自 std::async → 無法預知解構會不會阻塞 |
| 一致性 | 此行為在 C++11 與 C++14 完全相同(C++14 曾討論廢除但未採納) |
Shared State:結果存放在哪裡?
callee(通常透過 std::promise)把結果寫進通訊通道,caller 用 future 讀取。結果不能放在 callee 的 std::promise(callee 結束就被銷毀),也不能放在 caller 的 std::future(可能被轉成 std::shared_future 並複製多份,且 move-only 型別無法複製),所以放在兩者之外的 shared state:
+---------------------------+
| Shared State |
future | +---------------------+ | std::promise
Caller <~~~~~~~~~| Callee's Result |<~~~~~~~~~~~~~~~~ Callee
| +---------------------+ | (typically)
+---------------------------+
(通常為 heap 物件;型別/介面/實作由標準庫自行決定)
(~~~ 虛線箭頭 = 資訊由 callee 流向 caller)
future 解構子的行為由它所指涉的 shared state 決定——這是本 Item 的關鍵。shared state 內含參考計數,future 與 std::promise 都會操作它,讓標準庫知道 shared state 何時可銷毀(參考計數概念見 Item 19)。
正常行為 vs 唯一例外
| 行為 | 適用對象 | 解構子做什麼 |
|---|---|---|
| 正常行為 | 其餘所有 future | 只銷毀資料成員、遞減參考計數;對非同步任務等同隱含 detach;若是 deferred task 的最後一個 future,該任務永遠不會執行 |
| 例外行為 | 同時滿足下方三條件的 future | 阻塞直到任務完成——等同對執行該任務的執行緒做隱含 join |
例外行為必須三個條件全部成立:
- future 指涉的 shared state 來自
std::async的呼叫; - 該任務的 launch policy 是
std::launch::async(無論是明確指定,或由 runtime 系統自行選擇,見 Item 36); - 該 future 是指涉此 shared state 的最後一個 future——
std::future必然如此;std::shared_future則只有最後一份才會阻塞,其餘皆為正常行為。
這句常見的簡化說法忽略了:(1) launch policy 必須實際為 std::launch::async——若任務被 deferred 且從未執行,最後一個 future 解構只是讓任務永遠不跑,不會阻塞;(2) std::shared_future 只有最後一個指涉者才觸發阻塞;(3) 由 std::promise、std::packaged_task 建立的 shared state 一律走正常行為。
標準委員會想避免隱含 detach 的危害(懸掛參考等,見 Item 37),又不願像 joinable std::thread 那樣強制終止程式,於是折衷採用隱含 join。此決策有爭議,但 C++11 與 C++14 行為一致。
由於 future 的 API 無法查詢 shared state 是否來自 std::async,任何持有 future 的物件都可能在解構時阻塞:
// 此容器解構時可能阻塞:其中的 future 可能指涉
// std::async 啟動之非 deferred 任務的 shared state
std::vector<std::future<void>> futs;
class Widget { // Widget 物件的解構子
public: // 可能阻塞
// ...
private:
std::shared_future<double> fut;
};
std::packaged_task:確定走正常行為的例子
std::packaged_task 包裝可呼叫物件,使其結果寫入 shared state;由 get_future 取得的 future 必然不是 std::async 建立的,因此解構子必為正常行為:
int calcValue(); // 要執行的函式
{ // 區塊開始
std::packaged_task<int()>
pt(calcValue); // 包裝 calcValue 供非同步執行
auto fut = pt.get_future(); // 取得 pt 的 future
// → 確定不會在解構時阻塞
std::thread tmove(pt); // packaged_task 不可複製,
// 須以 std::move 轉為 rvalue(Item 23)
// ... // 對 t 的三種處置見下表
} // 區塊結束
在 ... 區域對 t 的三種可能處置:
對 t 的處置 |
後果 |
|---|---|
| 什麼都不做 | 區塊結尾 t 仍 joinable → 程式終止(Item 37) |
t.join() |
呼叫端已 join → fut 解構無須阻塞 |
t.detach() |
呼叫端已 detach → fut 解構無須 detach |
由 std::packaged_task 產生的 future 通常不需要特殊解構政策:終止 / join / detach 的抉擇已由操作底層 std::thread 的程式碼決定。這也解釋了 future 正常解構行為的合理性。另外,若打算用 std::async 執行任務,就沒必要先建 std::packaged_task——std::async 在排程前已做了同樣的事。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
| future 解構子「通常」做什麼 | 只銷毀資料成員並遞減 shared state 參考計數;不 join、不 detach、不執行任務 |
| future 解構子何時阻塞 | 三條件齊備:shared state 來自 std::async + policy 為 std::launch::async + 它是最後一個指涉的 future → 阻塞至任務完成(隱含 join) |
joinable std::thread 解構 vs future 解構 |
前者呼叫 std::terminate;後者永不終止程式,最多阻塞 |
std::shared_future 多份指涉同一 shared state |
只有最後一個被解構者可能阻塞,其餘只銷毀資料成員 |
| deferred task 的最後一個 future 被解構 | 任務永遠不會執行(也不阻塞) |
能否查詢 future 是否來自 std::async |
不能——API 未提供,故無法預知解構是否阻塞 |
std::packaged_task::get_future 取得的 future |
必為正常解構行為;join/detach 決策由操作 std::thread 的程式碼負責 |
packaged_task 傳入 std::thread 建構子 |
不可複製,須 std::move 轉為 rvalue |
| callee 的結果存哪裡 | shared state(caller 與 callee 之外,通常在 heap) |
| C++11 vs C++14 的 future 解構行為 | 相同(曾討論修改但未改) |
Related Notes
- 08-Concurrency-API/03-Threads-Unjoinable-On-All-Paths — Item 37:joinable
std::thread解構會終止程式,正是本 Item 對比的另一種 handle 行為 - 08-Concurrency-API/02-Std-Launch-Async — Item 36:launch policy 決定任務是否真正非同步,是觸發阻塞例外的條件之一
- 08-Concurrency-API/05-Void-Futures-For-Events — Item 39:future 通訊通道的其他用途與
std::future<void>、shared_future - 08-Concurrency-API/01-Task-Based-Vs-Thread-Based — Item 35:task-based 程式設計與
std::async的基礎 - 05-Smart-Pointers/02-Shared-Ptr — Item 19:shared state 內部參考計數的一般概念
- 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward — Item 23:
std::move將不可複製的packaged_task轉為 rvalue