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)
Important

future 解構子的行為由它所指涉的 shared state 決定——這是本 Item 的關鍵。shared state 內含參考計數,future 與 std::promise 都會操作它,讓標準庫知道 shared state 何時可銷毀(參考計數概念見 Item 19)。

正常行為 vs 唯一例外

行為 適用對象 解構子做什麼
正常行為 其餘所有 future 只銷毀資料成員、遞減參考計數;對非同步任務等同隱含 detach;若是 deferred task 的最後一個 future,該任務永遠不會執行
例外行為 同時滿足下方三條件的 future 阻塞直到任務完成——等同對執行該任務的執行緒做隱含 join

例外行為必須三個條件全部成立

  1. future 指涉的 shared state 來自 std::async 的呼叫;
  2. 該任務的 launch policy 是 std::launch::async(無論是明確指定,或由 runtime 系統自行選擇,見 Item 36);
  3. 該 future 是指涉此 shared state 的最後一個 future——std::future 必然如此;std::shared_future 則只有最後一份才會阻塞,其餘皆為正常行為。
「來自 std::async 的 future 會在解構子阻塞」只是第一近似

這句常見的簡化說法忽略了:(1) launch policy 必須實際為 std::launch::async——若任務被 deferred 且從未執行,最後一個 future 解構只是讓任務永遠不跑,不會阻塞;(2) std::shared_future 只有最後一個指涉者才觸發阻塞;(3) 由 std::promisestd::packaged_task 建立的 shared state 一律走正常行為

為什麼是隱含 join 而不是終止程式?

標準委員會想避免隱含 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
Tip

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 解構行為 相同(曾討論修改但未改)