《Effective Modern C++》條款 38 指出:std::thread、std::future(非推遲任務)均對應底層系統執行線程,但它們的析構函數行為截然不同。理解這些差異對編寫正確、高效的併發代碼至關重要。本文深度解析三者析構語義、設計權衡、實戰陷阱與最佳實踐。
| 類型 | 可連接/有效時 | 析構行為 | 設計動機 |
|---|---|---|---|
std::thread | joinable() == true | 調用 std::terminate() 終止程序 | 隱式 join 可能導致死鎖/性能問題;隱式 detach 可能導致資源洩漏/未定義行為。終止程序強制開發者顯式處理。 |
std::future (來自 std::async + launch::async) | 最後一個引用 shared state 的 future | 阻塞等待任務完成(隱式 join) | 避免隱式 detach 的風險,又不像 thread 那樣激進終止程序。妥協方案。 |
std::future (其他來源:promise/packaged_task/推遲任務) | 任意 | 僅銷毀 future 對象本身(減少引用計數),不阻塞、不 join、不 detach | 正常 RAII 行為,共享狀態由其他持有者管理。 |
std::shared_future | 非最後一個引用 | 正常銷毀,僅減少引用計數 | 共享所有權語義,最後一個才觸發特殊行為。 |
std::async 創建。std::launch::async(顯式指定或運行時選擇)。std::future 總是唯一擁有者;std::shared_future 需為最後一個析構者)。只有三條全滿足時,析構才會阻塞等待任務結束。
std::thread 那樣直接 terminate,給予「等待完成」的安全妥協。
// 陷阱 1:vector 析構可能阻塞
std::vector> futures;
for (int i = 0; i < 10; ++i) {
futures.push_back(std::async(std::launch::async, []{ /* 耗時任務 */ }));
}
// 離開作用域時,vector 析構 → 逐個 future 析構 → 依次阻塞等待!
// 後提交的任務雖已並行啟動,但析構時按順序串行等待,總耗時 = 所有任務耗時之和。
// 陷阱 2:類成員 future 析構阻塞
class Widget {
std::shared_future fut_; // 來自 std::async
public:
~Widget() { } // 析構時若 fut_ 為最後一個引用且為 async 任務,將阻塞!
};
// 方案 A:在析構前顯式 get() / wait()
for (auto& f : futures) f.get(); // 或 f.wait()
futures.clear(); // 此時 future 已就緒,析構不阻塞
// 方案 B:使用 packaged_task + thread 替代 async
std::packaged_task task(work);
std::future fut = task.get_future();
std::thread t(std::move(task));
// t.join() 或 t.detach() 由開發者顯式控制
// fut 析構時不會阻塞(非 async 產生的 shared state)
C++ 標準庫**不提供**查詢 future 是否來自 std::async 的接口。因此:
std::packaged_task、std::promise 或推遲任務 → 析構**不阻塞**。std::async 且未指定 launch::deferred → **可能阻塞**(視運行時策略)。get() 或 wait()。
int calcValue(); // 耗時計算
{
std::packaged_task pt(calcValue);
auto fut = pt.get_future(); // future 綁定 pt 的 shared state
std::thread t(std::move(pt)); // 移動任務到線程執行
// 此處三種選擇,任選其一:
// 1. t.join(); // 等待線程結束,fut.get() 立即可得結果,不阻塞
// 2. t.detach(); // 分離線程,fut.get() 仍會阻塞等待任務完成(但非析構阻塞)
// 3. 什麼都不做 → t 析構時 terminate(因 joinable)!
// 推薦:顯式 join
t.join();
int result = fut.get(); // 非阻塞獲取結果
} // fut 析構:正常銷毀,無特殊行為
關鍵點:packaged_task 產生的 shared state **不屬於 async 特殊規則**,其 future 析構行為為「正常銷毀」。線程管理由開發者顯式控制(join/detach/terminate 三選一),更清晰可控。
編寫併發代碼時,**永遠顯式調用 get()/wait()/join()**,不要依賴析構函數的隱式行為,這是避免死鎖、性能陷阱、邏輯錯誤的根本之道。