首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

Effective Modern C++ 條款 38:深度理解 std::thread 與 future 析構行為差異

     編輯:關於C++

前言

《Effective Modern C++》條款 38 指出:std::thread、std::future(非推遲任務)均對應底層系統執行線程,但它們的析構函數行為截然不同。理解這些差異對編寫正確、高效的併發代碼至關重要。本文深度解析三者析構語義、設計權衡、實戰陷阱與最佳實踐。

一、核心對比表

類型可連接/有效時析構行為設計動機
std::threadjoinable() == 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非最後一個引用正常銷毀,僅減少引用計數共享所有權語義,最後一個才觸發特殊行為。

二、future 異常行為觸發條件(必須同時滿足)

  1. future 的 shared state 由 std::async 創建。
  2. 啟動策略為 std::launch::async(顯式指定或運行時選擇)。
  3. 該 future 是**最後一個**引用該 shared state 的 future(std::future 總是唯一擁有者;std::shared_future 需為最後一個析構者)。

只有三條全滿足時,析構才會阻塞等待任務結束。

三、為什麼要這樣設計?

  • 避免隱式 detach 風險:detach 後線程獨立運行,主線程無法同步、異常傳播困難、程序退出時可能還在訪問已銷毀對象(條款 37)。
  • 避免隱式 join 死鎖:若析構隱式 join,持有鎖的線程析構時可能等待自己永不釋放的鎖。
  • 不終止程序:不像 std::thread 那樣直接 terminate,給予「等待完成」的安全妥協。
  • 標委會曾討論 C++14 禁用此行為,但最終保留,C++11/14/17/20 行為一致。

四、實戰陷阱:容器與類成員析構可能阻塞


// 陷阱 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 任務,將阻塞!
};

五、規避策略與最佳實踐

1. 顯式管理:提前 join / 分離等待


// 方案 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)

2. 判斷 future 來源:無標準 API,靠代碼邏輯保證

C++ 標準庫**不提供**查詢 future 是否來自 std::async 的接口。因此:

  • 若確定 future 來自 std::packaged_task、std::promise 或推遲任務 → 析構**不阻塞**。
  • 若來自 std::async 且未指定 launch::deferred → **可能阻塞**(視運行時策略)。
  • 最安全:不要依賴析構行為,顯式調用 get() 或 wait()。

3. packaged_task + thread 模式詳解


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 三選一),更清晰可控。

六、總結:必記兩條鐵律

  1. future 析構通常只是銷毀成員、減少引用計數,不做任何同步等待。
  2. 唯一例外:最後一個引用「std::async 啟動的非推遲任務」shared state 的 future,析構時會阻塞等待任務完成(隱式 join)。

編寫併發代碼時,**永遠顯式調用 get()/wait()/join()**,不要依賴析構函數的隱式行為,這是避免死鎖、性能陷阱、邏輯錯誤的根本之道。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved