執行緒的啟動與等待 (Launching and Joining Threads)
Overview Table
| 概念 | 重點 |
|---|---|
| 啟動執行緒 | 建構 std::thread 物件即啟動;可傳入函式、函式物件、lambda(需 <thread> 標頭) |
| Most Vexing Parse | 傳入臨時函式物件會被解析成函式宣告;用雙括號、{} 或 lambda 迴避 |
| join vs detach | std::thread 解構前必須二選一,否則解構函式呼叫 std::terminate() |
| 異常安全等待 | catch 區塊補 join,或用 RAII 的 thread_guard 在解構函式中 join |
| 後台執行緒 | detach() 後成為守護執行緒 (daemon thread),無法再 join |
啟動執行緒
每個程式至少有一條執行 main() 的執行緒;其餘執行緒在建構 std::thread 物件時啟動,與主執行緒同時執行。可傳入的 callable 有三類:
- 一般函式:
std::thread my_thread(do_some_work); - 函式物件(有
operator()的類別實例):物件會被複製到新執行緒的儲存空間中,執行與呼叫都在該執行緒的記憶體空間進行 - lambda 表達式:可捕獲區域變數,常可省去傳參
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()}; // ✓ 統一初始化語法 {}
std::thread my_thread(background_task());相當於宣告名為my_thread、回傳std::thread的函式,其參數是函式指標- 迴避方式:命名變數、雙括號、大括號
{}、lambda
這種程式碼能通過編譯,不會有任何錯誤訊息——執行緒根本沒啟動,是典型的沉默錯誤。
join vs detach:解構前必須做出決定
執行緒啟動後,必須在 std::thread 物件銷毀前決定「等待thread 的解構函式會呼叫 std::terminate( 終止整個程式。
| 面向 | join() |
detach() |
|---|---|---|
| 語意 | 等待執行緒結束(匯入) | 放到後台自主執行 |
呼叫後 joinable() |
false |
false |
| 資源 | join 時清理執行緒相關記憶體 | 執行緒結束時由 C++ 執行期函式庫回收 |
| 可否再互動 | 物件與已完成的執行緒不再關聯 | 無任何 std::thread 物件能再引用它 |
| 區域變數安全 | 保證區域變數在執行緒完成後才銷毀 | 必須自行保證資料有效性(通常改用拷貝) |
join()只能呼叫一次;join 過後joinable()回傳falsedetach()同樣要求joinable()為true才能呼叫- 想「只等一段時間」或「檢查是否結束」需改用條件變數 (condition variable) 或 future(見第 4 章)
對
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 ✗ 未定義行為
- 常規解法:把資料複製到執行緒中,而非共享指標/參考
- 用「存取區域變數的函式」建立執行緒是糟糕的主意;或改以
join()確保執行緒先結束
特殊情況下的等待:異常安全與 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比std::thread先解構,於解構函式中完成 join,即使拋出例外也會發生 - 解構函式中先判斷
joinable()再 join(join 只能一次) - 拷貝建構/拷貝賦值標記
= delete,防止物件生命週期超過所匯入執行緒的作用域
持有「參考」的 thread_guard 只負責等待;若要連所有權一起接管並在建構時檢查,見 02-Thread-Management/02-Passing-Arguments-and-Transferring-Ownership 的
scoped_thread。後台執行緒(守護執行緒)
detach() 讓執行緒在後台執行,與主執行緒不能直接互動,也不可能再有 std::thread 物件引用它:
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());
- 分離執行緒又稱守護執行緒 (daemon threads):仿 UNIX 中無顯式介面、長時間在後台執行的執行緒(監視檔案系統、清理快取、最佳化資料結構等)
- 適合發後即忘 (fire and forget) 的任務
- C++ 執行期函式庫保證執行緒退出時正確回收相關資源
- 典型範例(代碼 2.4):文書處理器每開一份新文件就
std::thread t(edit_document, new_name); t.detach();——各視窗處理獨立文件,無須互相等待
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 |
Related Notes
- 02-Thread-Management/02-Passing-Arguments-and-Transferring-Ownership
- 02-Thread-Management/03-Thread-Count-and-Identification
- 02-Thread-Management/Practice-Thread-Management
- 01-Concurrency-Fundamentals/03-Cpp-Concurrency-Support-and-First-Program
- 08-Designing-Concurrent-Code/04-Exception-Safety-and-Scalability