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

Effective Modern C++ 條款 37:確保 std::thread 在所有路徑上皆不可連接

     編輯:關於C++

確保 std::thread 在所有執行路徑上變為不可連接

每個 std::thread 物件皆處於兩種狀態之一:可連接 或 不可連接。可連接的 std::thread 對應一個真實存在的底層執行緒——該執行緒可能正在運行、被阻塞、或等待排程。不可連接的 std::thread 則無對應的底層執行緒,主要包含以下四種情況:

  • 預設建構:無可執行函式,故無底層執行緒。
  • 被移動後:所有權轉移給另一個 std::thread。
  • 已呼叫 join():底層執行緒已結束,無對應執行緒。
  • 已呼叫 detach():std::thread 與底層執行緒分離。

std::thread 的可連接性至關重要,原因在於:若可連接的執行緒物件被解構,程式會被終止。這是標準委員會為避免隱式 join 或 detach 導致更嚴重問題(效能異常、未定義行為)而做出的設計決策。

問題示範

假設有一個函式 doWork,需在背景執行緒執行耗時的過濾運算,主執行緒則檢查條件決定是否等待結果:

constexpr auto tenMillion = 10'000'000; // C++14 數字分隔符

bool doWork(std::function<bool(int)> filter, int maxVal = tenMillion) {
    std::vector<int> goodVals;
    std::thread t([&filter, maxVal, &goodVals] {
        for (auto i = 0; i <= maxVal; ++i) {
            if (filter(i)) goodVals.push_back(i);
        }
    });

    auto nh = t.native_handle(); // 取得原生句柄以設定優先權
    // ... 使用 nh 設定執行緒優先權 ...

    if (conditionsAreSatisfied()) {
        t.join();
        performComputation(goodVals);
        return true;
    }
    return false; // 危險!t 仍可連接,析構時會 terminate
}

若 conditionsAreSatisfied() 回傳 false 或拋出異常,t 在離開作用域時仍為可連接狀態,導致程式終止。

為何不隱式 join 或 detach?

  • 隱式 join:會導致難以追蹤的效能異常。呼叫端明明不想等待,卻被迫阻塞。
  • 隱式 detach:會造成更難除錯的未定義行為。區域變數(如 goodVals)在函式返回時銷毀,但脫離的執行緒仍持有其引用,導致堆疊記憶體被破壞,除錯極其困難。

解決方案:RAII 包裝類別

在所有路徑上確保 std::thread 變為不可連接,最可靠的方式是使用 RAII(資源獲取即初始化)模式。標準庫未提供 std::thread 的 RAII 包裝器,因為委員會不想預設選擇 join 或 detach。但我們可自行實作:

class ThreadRAII {
public:
    enum class DtorAction { join, detach };

    ThreadRAII(std::thread&& t, DtorAction a)
        : action(a), t(std::move(t)) {}

    ~ThreadRAII() {
        if (t.joinable()) {
            if (action == DtorAction::join) {
                t.join();
            } else {
                t.detach();
            }
        }
    }

    std::thread& get() { return t; }

    // 支援移動語意
    ThreadRAII(ThreadRAII&&) = default;
    ThreadRAII& operator=(ThreadRAII&&) = default;

private:
    DtorAction action;
    std::thread t;
};

關鍵設計細節:

  • 建構函數接受右值參考:std::thread 不可複製,只能移動。
  • 成員變數宣告順序:將 std::thread 放在最後,確保先初始化其他成員,避免執行緒啟動時存取未初始化的資料。
  • 提供 get():類似智慧指標,允許存取內部 std::thread 以呼叫 native_handle() 等介面。
  • 析構前檢查 joinable():對不可連接的執行緒呼叫 join/detach 屬未定義行為。
  • 無競爭條件:std::thread 狀態只能透過成員函式改變,析構函數執行時不應有其他執行緒同時呼叫該物件的成員函式。

改寫後的 doWork

bool doWork(std::function<bool(int)> filter, int maxVal = tenMillion) {
    std::vector<int> goodVals;

    ThreadRAII t(
        std::thread([&filter, maxVal, &goodVals] {
            for (auto i = 0; i <= maxVal; ++i) {
                if (filter(i)) goodVals.push_back(i);
            }
        }),
        ThreadRAII::DtorAction::join // 析構時 join
    );

    auto nh = t.get().native_handle();
    // ... 設定優先權 ...

    if (conditionsAreSatisfied()) {
        t.get().join(); // 明確 join,RAII 析構時 t 已不可連接
        performComputation(goodVals);
        return true;
    }
    return false; // 安全:RAII 析構會 join
}

此處選擇 join 而非 detach,因為 detach 會導致區域變數生命週期問題。雖然 join 可能造成效能異常(條款 39 將討論如何避免),但比起未定義行為或程式終止,效能異常是三害中最輕微者。

總結

  1. 在所有路徑上,讓 std::thread 變得不可連接。
  2. 析構時隱式 join 會導致難以除錯的效能異常。
  3. 析構時隱式 detach 會導致難以除錯的未定義行為。
  4. 在成員變數列表最後宣告 std::thread。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved