執行緒的啟動與等待 (Launching and Joining Threads)

Overview Table

概念 重點
啟動執行緒 建構 std::thread 物件即啟動;可傳入函式、函式物件、lambda(需 <thread> 標頭)
Most Vexing Parse 傳入臨時函式物件會被解析成函式宣告;用雙括號、{} 或 lambda 迴避
join vs detach std::thread 解構前必須二選一,否則解構函式呼叫 std::terminate()
異常安全等待 catch 區塊補 join,或用 RAIIthread_guard 在解構函式中 join
後台執行緒 detach() 後成為守護執行緒 (daemon thread),無法再 join

啟動執行緒

每個程式至少有一條執行 main() 的執行緒;其餘執行緒在建構 std::thread 物件時啟動,與主執行緒同時執行。可傳入的 callable 有三類:

class background_task {
public:
    void operator()() const { do_something(); do_something_else(); }
};
background_task f;
std::thread my_thread(f);              // 函式物件(會被複製)

std::thread my_thread2([]{             // lambda
    do_something(); do_something_else();
});

Most Vexing Parse 陷阱

若把臨時函式物件直接傳給 std::thread 建構函式,C++ 編譯器會將其解析為函式宣告,而非物件定義:

std::thread my_thread(background_task());   // ✗ 宣告了一個「函式」!
std::thread my_thread((background_task())); // ✓ 多一組括號
std::thread my_thread{background_task()};   // ✓ 統一初始化語法 {}
這種程式碼能通過編譯,不會有任何錯誤訊息——執行緒根本沒啟動,是典型的沉默錯誤。

join vs detach:解構前必須做出決定

執行緒啟動後,必須在 std::thread 物件銷毀前決定「等待thread 的解構函式會呼叫 std::terminate( 終止整個程式。

面向 join() detach()
語意 等待執行緒結束(匯入) 放到後台自主執行
呼叫後 joinable() false false
資源 join 時清理執行緒相關記憶體 執行緒結束時由 C++ 執行期函式庫回收
可否再互動 物件與已完成的執行緒不再關聯 無任何 std::thread 物件能再引用它
區域變數安全 保證區域變數在執行緒完成後才銷毀 必須自行保證資料有效性(通常改用拷貝)
joinable()false 的物件呼叫 join()/detach() 會拋出 std::system_error(標準補充);使用前先以 joinable() 檢查。

detach 的懸空參考危機(代碼 2.1)

若執行緒函式持有區域變數的指標或參考,而函式先於執行緒結束,就會產生未定義行為:

主執行緒                              新執行緒
────────                              ────────
建構 my_func(持有 some_local_state 的參考)
啟動 my_thread ──────────────────────▶ 開始執行 func::operator()
my_thread.detach()                     do_something(i) 使用該參考
銷毀 some_local_state                  持續執行
退出 oops()                            再存取 some_local_state ✗ 未定義行為

特殊情況下的等待:異常安全與 thread_guard

join() 的呼叫位置需要細心挑選:若 join 之前拋出例外,join 會被跳過,導致 std::thread 帶著關聯執行緒被解構而 terminate。

做法一:try/catch 中補 join(代碼 2.2)

std::thread t(my_func);
try { do_something_in_current_thread(); }
catch (...) {
    t.join();   // 異常路徑也要 join
    throw;
}
t.join();       // 正常路徑

做法二:RAII——thread_guard(代碼 2.3,推薦)

class thread_guard {
    std::thread& t;                    // 只持有參考,不擁有
public:
    explicit thread_guardthread& t_) : t(t_ {}
    ~thread_guard() { if (t.joinable()) t.join(); }   // 解構時保證匯入
    thread_guard(thread_guard const&) = delete;       // 禁止拷貝
    thread_guard& operator=(thread_guard const&) = delete;
};
持有「參考」的 thread_guard 只負責等待;若要連所有權一起接管並在建構時檢查,見 02-Thread-Management/02-Passing-Arguments-and-Transferring-Ownershipscoped_thread

後台執行緒(守護執行緒)

detach() 讓執行緒在後台執行,與主執行緒不能直接互動,也不可能再有 std::thread 物件引用它:

std::thread t(do_background_work);
t.detach();
assert(!t.joinable());

Exam/Test Patterns

情境/關鍵字 答案
std::thread 解構前未 join 也未 detach 解構函式呼叫 std::terminate(),程式終止
std::thread t(Task()); 編譯通過但執行緒「沒啟動」 most vexing parse——被解析為函式宣告;改用 {}、雙括號、命名變數或 lambda
detach 後執行緒存取已銷毀的區域變數 未定義行為;解法:把資料拷貝進執行緒,或改用 join
「例外拋出導致跳過 join」 catch 中 join 後 re-throw,或使用 RAII thread_guard
join 之後再 join / 判斷能否 join join 只能一次;以 joinable() 檢查,join/detach 後回傳 false
「長期後台任務、發後即忘」 detach() → 守護執行緒 (daemon thread),不可再 join