中斷執行緒 (Interrupting Threads)
Overview Table
| 概念 | 重點 |
|---|---|
interruptible_thread 介面(9.2.1) |
std::thread 介面 + interrupt();thread_local interrupt_flag + promise 傳遞旗標指標 |
interruption_point()(9.2.2) |
檢查本執行緒旗標,已設定就拋出 thread_interrupted 異常 —— 協作式中斷 |
| 中斷條件變數等待(9.2.3) | 天真版有異常安全 + 競爭條件兩問題;實務解法:wait_for() 1ms 超時輪詢 |
condition_variable_any 版(9.2.4) |
custom_lock 連鎖 set_clear_mutex,徹底消除競爭,不靠超時 |
| 中斷其他阻塞呼叫(9.2.5) | future 等:迴圈 wait_for(1ms) + 檢查旗標;受時鐘精度限制 |
| 處理中斷(9.2.6) | catch (thread_interrupted&);不攔截會進 std::thread 解構 → std::terminate() |
| 退出時中斷後台執行緒(9.2.7) | 先全部 interrupt() 再逐一 join(),讓各執行緒並行處理中斷 |
為什麼需要中斷?(9.2)
長時間執行的執行緒常需被要求「提早停止」:結果已確定、發生錯誤、使用者取消、或池要銷毀。重點是讓執行緒主動、有秩序地停下(清理資源、保持資料結構一致),而非戛然而止。C++11 標準沒有提供此機制,但自行實作不難;統一機制比逐場景硬寫更易維護。
本節機制對應提案 P0660(書中註腳),後來成為 C++20 的 std::jthread + std::stop_token:request_stop() ≈ interrupt()、stop_requested() ≈ 旗標檢查,並且 condition_variable_any::wait() 有接受 stop_token 的重載。
interruptible_thread 介面與基本實作(9.2.1,代碼 9.9)
介面 = std::thread 的介面(join()/detach()/joinable())再加一個 interrupt()。
核心機制:
thread_local interrupt_flag this_thread_interrupt_flag—— 每條執行緒有自己的中斷旗標;interruption_point()檢查的就是當前執行緒的這份 thread_local 資料- 因為 thread_local 旗標必須「屬於新執行緒、卻能被擁有者設定」,不能直接用普通
std::thread管理 —— 建構函式用 promise/future 把旗標位址傳回來:
template <typename FunctionType>
interruptible_thread(FunctionType f) {
std::promise<interrupt_flag*> p;
internal_thread = std::thread([f, &p] {
p.set_value(&this_thread_interrupt_flag); // 新執行緒回報旗標位址
f();
});
flag = p.get_future().get(); // 等到位址才返回
}
void interrupt() { if (flag) flag->set(); }
- Lambda 引用區域變數
p沒有懸空問題:建構函式會等 future 就緒才返回,屆時p已不再被引用 - 需注意在執行緒退出或 detach 前避免
flag懸空(書中實作未處理 join/detach 細節)
檢查中斷:interruption_point()(9.2.2)
void interruption_point() {
if (this_thread_interrupt_flag.is_set())
throw thread_interrupted();
}
在迴圈等安全的地方顯式呼叫(例:while (!done) { interruption_point(); process_next_item(); })。
這是純輪詢式檢查的致命弱點:執行緒卡在條件變數 wait() 或 future 等待時根本不在執行,無法檢查旗標 → 需要 interruptible_wait() 系列函式。
中斷條件變數等待(9.2.3,代碼 9.10–9.11)
天真版:interruption_point() → set_condition_variable(cv)(讓 interrupt_flag::set() 能 notify_all() 喚醒等待者)→ cv.wait(lk) → 清除關聯 → 再檢查。有兩個問題:
- 異常安全:
cv.wait()可能拋出異常,直接離開函式而沒清除旗標與條件變數的關聯 → 修法:用 RAII 結構(clear_cv_on_destruct)在解構時清除 - 競爭條件 (race condition):最後一次
interruption_point()檢查之後、cv.wait()真正開始等待之前,若旗標被設定並notify_all(),通知會遺失(執行緒還不在等待狀態)→ 之後就再也叫不醒了
想把 lk 傳進 set_condition_variable(),讓 interrupt() 也鎖同一把互斥鎖 —— 但這要求把互斥鎖引用交給生命週期未知的中斷方執行緒,可能死結 (deadlock) 或存取已銷毀的互斥鎖,不可取。
實務解法(代碼 9.11):放棄「完美喚醒」,改用超時等待 cv.wait_for(lk, 1ms) —— 就算通知遺失,最多 1ms 後醒來檢查旗標,把等待的上限交給時鐘。
interrupt_flag成員:std::atomic<bool> flag、std::condition_variable* thread_cond、std::mutex set_clear_mutexset():先flag.store(true, memory_order_relaxed),再鎖set_clear_mutex、若thread_cond非空則notify_all()- 帶述詞版把 1ms 超時完全藏進迴圈:
while (!this_thread_interrupt_flag.is_set() && !pred())
cv.wait_for(lk, std::chrono::milliseconds(1));
代價:虛假喚醒與述詞檢查次數大增。
用 condition_variable_any 徹底解決(9.2.4,代碼 9.12)
std::condition_variable_any 可搭配任意符合 Lockable 的鎖,不限 std::unique_lock<std::mutex> —— 於是可以設計 custom_lock,把 interrupt_flag 內部的 set_clear_mutex 與使用者的鎖綁在一起:
custom_lock 生命週期 持有的鎖
建構 lock(set_clear_mutex) + 設 thread_cond_any [兩把都鎖住]
cv.wait(cl) 內部 unlock() → 同時解 lk 與 set_clear_mutex [進入等待,原子地]
喚醒 lock() → std::lock(set_clear_mutex, lk) [兩把一起鎖回]
解構 清 thread_cond_any + 解 set_clear_mutex
- 競爭為何消失:
interrupt()的set()必須先取得set_clear_mutex才能通知;而 custom_lock 從「檢查中斷」到「wait 進入等待狀態」全程持有set_clear_mutex→ 旗標若已設定,必然在檢查時看見;若還沒設定,notify_all()必然發生在等待開始之後 → 通知不可能遺失,不需 1ms 輪詢 lock()用std::lock(self->set_clear_mutex, lk)同時鎖兩把,避免死結- 解構函式清掉
thread_cond_any指標並解鎖,之後可再次檢查中斷
中斷其他阻塞呼叫(9.2.5)
互斥鎖、future 等阻塞沒有「掛上通知」的入口,一般只能沿用超時輪詢:
template <typename T>
void interruptible_waitfuture<T>& uf {
while (!this_thread_interrupt_flag.is_set()) {
if milliseconds(1) ==
std::future_status::ready) break;
}
interruption_point();
}
wait_for 至少等一個時鐘刻度:若刻度是 15ms,實際每輪是 15ms 而非 1ms。縮短超時能加快反應,但頻繁喚醒檢查旗標會增加執行緒切換開銷 —— 反應速度與開銷之間的權衡。
處理中斷:異常 + catch(9.2.6)
中斷在被中斷執行緒眼中就是 thread_interrupted 異常,用標準 catch 處理即可;處理完後若再被 interrupt(),下個中斷點 (interruption point) 會再拋 —— 適合「一系列獨立任務,中斷即放棄當前任務、續做下一個」的模型。
- 異常安全注意事項照舊適用:確保資源不洩漏、資料結構留下一致狀態
- 未捕獲的
thread_interrupted傳進std::thread解構時會呼叫std::terminate()終止整個程式 →interruptible_thread的包裝 lambda 中把f()包進try { f(); } catch (thread_interrupted const&) {},保證未處理的中斷也異常安全
應用退出時中斷後台執行緒(9.2.7,代碼 9.13)
桌面搜尋類應用:GUI 執行緒保持回應,後台執行緒監視檔案系統、更新索引,直到應用退出。
for (auto& t : background_threads) t.interrupt(); // 1. 先全部發中斷
for (auto& t : background_threads) t.join(); // 2. 再逐一等待
- 後台迴圈每輪呼叫
interruption_point()檢查中斷 - 為何不「中斷一個就 join 一個」? 並發!被中斷的執行緒不會立即結束(要走到下個中斷點、執行解構與異常處理)。先發完所有中斷,各執行緒可並行處理自己的中斷,整體退出更快
主執行緒 後台執行緒 (每一條)
interrupt() ──► set flag ──► ... 下一個 interruption_point()
interrupt() ──► set flag └─ throw thread_interrupted
│ └─ 解構/清理 → catch → 執行緒結束
join() ◄────────────────────────────┘ (所有執行緒並行清理)
Exam/Test Patterns
| 情境/關鍵字 | 答案 |
|---|---|
| 「如何讓執行緒可被中斷?」 | interruptible_thread:std::thread 介面 + interrupt();thread_local interrupt_flag + 中斷點檢查 |
| 「旗標位址如何從新執行緒傳回建構函式?」 | promise/future:lambda 內 p.set_value(&this_thread_interrupt_flag),建構函式 get() 等待 |
「interruption_point() 做什麼?」 |
檢查當前執行緒旗標,已設定 → 拋出 thread_interrupted |
「執行緒卡在 cv.wait(),中斷不了」 |
需 interruptible_wait():設定旗標時 notify_all(),或超時輪詢 |
| 「天真 interruptible_wait 的兩個 bug」 | (1) wait() 拋異常沒清關聯 → RAII;(2) 檢查與 wait 之間的競爭 → 通知遺失 |
「std::condition_variable 版最後怎麼解競爭?」 |
不完美解:wait_for(1ms) 超時輪詢,通知遺失最多晚 1ms 發現 |
「condition_variable_any 版為何無競爭?」 |
custom_lock 全程持有 set_clear_mutex;set() 需同把鎖才能通知 → 通知不可能落在空窗 |
| 「等待 future 如何中斷?」 | 迴圈 wait_for(1ms) + 檢查旗標;受時鐘刻度限制 |
| 「中斷異常沒人接會怎樣?」 | 傳入 std::thread 解構 → std::terminate();包裝 lambda 內 catch (thread_interrupted) |
| 「多條後台執行緒如何優雅退出?」 | 先全部 interrupt(),再逐一 join() —— 讓中斷處理並行 |
| 「C++20 的對應機制?」 | std::jthread + std::stop_token(源自 P0660) |
Related Notes
- 09-Advanced-Thread-Management/01-Thread-Pools
- 09-Advanced-Thread-Management/02-Work-Stealing-and-Queue-Contention
- 09-Advanced-Thread-Management/Practice-Advanced-Thread-Management
- 04-Synchronizing-Operations/01-Condition-Variables-and-Thread-Safe-Queue
- 04-Synchronizing-Operations/03-Waiting-with-Time-Limits
- 02-Thread-Management/01-Launching-and-Joining-Threads