中斷執行緒 (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()

核心機制:

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(); }

檢查中斷: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(); })。

阻塞中的執行緒呼叫不到 interruption_point()

這是純輪詢式檢查的致命弱點:執行緒卡在條件變數 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) → 清除關聯 → 再檢查。有兩個問題:

  1. 異常安全:cv.wait() 可能拋出異常,直接離開函式而沒清除旗標與條件變數的關聯 → 修法:用 RAII 結構(clear_cv_on_destruct)在解構時清除
  2. 競爭條件 (race condition):最後一次 interruption_point() 檢查之後、cv.wait() 真正開始等待之前,若旗標被設定並 notify_all(),通知會遺失(執行緒還不在等待狀態)→ 之後就再也叫不醒了
為什麼不能「用 lk 的互斥鎖保護」?

想把 lk 傳進 set_condition_variable(),讓 interrupt() 也鎖同一把互斥鎖 —— 但這要求把互斥鎖引用交給生命週期未知的中斷方執行緒,可能死結 (deadlock) 或存取已銷毀的互斥鎖,不可取。

實務解法(代碼 9.11):放棄「完美喚醒」,改用超時等待 cv.wait_for(lk, 1ms) —— 就算通知遺失,最多 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

中斷其他阻塞呼叫(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) 會再拋 —— 適合「一系列獨立任務,中斷即放棄當前任務、續做下一個」的模型。

應用退出時中斷後台執行緒(9.2.7,代碼 9.13)

桌面搜尋類應用:GUI 執行緒保持回應,後台執行緒監視檔案系統、更新索引,直到應用退出。

for (auto& t : background_threads) t.interrupt();  // 1. 先全部發中斷
for (auto& t : background_threads) t.join();       // 2. 再逐一等待
 主執行緒                       後台執行緒 (每一條)
 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)