題干與適用場景
你需要實作一個背景 worker,持續從佇列讀取任務。服務關閉或上游取消時,主執行緒請求 worker 停止;已開始的任務要完成,尚未開始的任務可以保留或明確丟棄。實作必須避免忙等、資料競爭、解構時遺留執行緒和條件變數永久等待。
C++20 的 std::jthread 會在解構時請求停止並 join;它可以向入口函式注入 std::stop_token。停止請求是協作訊號,不會強制終止任意程式碼;worker 必須輪詢或使用可回應 stop token 的等待。
面試官考察點
- 是否理解 stop token 是共享狀態上的請求,不是非同步殺執行緒。
- 是否使用
std::jthread的自動 join,並避免執行緒仍存取物件成員時解構物件。 - 是否讓阻塞等待能被停止喚醒,例如
conditionvariableany::wait的 stop-token 多載。 - 是否保證佇列、停止狀態、任務例外和資源清理的生命週期安全。
- 是否測試停止競態、空佇列、任務拋例外、重複 request_stop 和解構順序。
回答前需要釐清的問題
- 停止時正在執行的任務能否完成,任務是否具冪等性或有外部副作用?
- 佇列關閉後,未消費任務是丟棄、轉移還是等待另一個 worker?
- worker 的等待是否支援
conditionvariableany,還是依賴不可中斷的第三方 I/O? - 例外由 worker 記錄、傳給 join 方,還是觸發整體關閉?
- 物件是否可能由多個執行緒呼叫 stop、解構或重新啟動?
30 秒回答框架
「用 std::jthread 持有 worker,讓入口接收 std::stoptoken。佇列用互斥鎖保護,等待時使用能回應 token 的 conditionvariableany;喚醒後先判斷 stop、佇列關閉和任務狀態。已取出的任務執行到明確的取消檢查點,再釋放資源。解構依靠 jthread 的 requeststop 加 join,禁止 detach。所有共享狀態的生命週期覆蓋執行緒,並測試停止競態和例外。」
分步驟深入解答
第一步:定義取消契約
把停止分為「請求停止」和「任務完成」。stop 請求只阻止 worker 取得新任務;已開始任務在安全檢查點完成或回傳可識別的取消結果。不能承諾任意第三方呼叫會立即終止。
第二步:設計執行緒與狀態所有權
讓擁有佇列、互斥鎖、條件變數和 std::jthread 的物件活得比執行緒久。解構先請求停止並等待,再釋放成員。禁止捕獲已離開作用域的引用。
第三步:讓阻塞等待可取消
使用 conditionvariableany 的 stop-token 等待,或註冊 stopcallback 呼叫 notifyall。謂詞同時檢查佇列非空、佇列關閉和 stop_requested();被喚醒後重新持鎖判斷。
std::jthread worker([this](std::stop_token st) {
for (;;) {
Task task;
{
std::unique_lock lock(mu_);
cv_.wait(lock, st, [this, &st] {
return closed_ || !queue_.empty() || st.stop_requested();
});
if (st.stop_requested() || (closed_ && queue_.empty())) return;
task = std::move(queue_.front());
queue_.pop_front();
}
run(task, st);
}
});第四步:處理任務中的停止點和例外
長任務在分段操作之間檢查 token;檢查點要定義副作用邊界,避免半寫入。捕獲例外後記錄任務 ID 和錯誤,決定是否繼續消費或請求整體停止。
第五步:處理關閉順序
關閉流程先禁止新任務,再通知佇列和 stop token,等待 worker 退出,最後釋放資源。明確 drain 超時和剩餘任務處理;重複 request_stop() 應安全。
第六步:驗證競態和可觀測性
測試空佇列停止、任務剛出隊時停止、多執行緒同時停止、解構期間通知、任務拋例外、阻塞 I/O 超時和重複關閉。記錄停止延遲、完成/取消任務數、佇列剩餘、例外和 join 時間;用 ThreadSanitizer 檢查資料競爭。
高品質示範回答
「我用 jthread 管理 worker 生命週期,入口接收 stoptoken。佇列狀態由 mutex 保護,conditionvariable_any 的謂詞檢查關閉、非空和 stop 請求;停止會喚醒等待。worker 取出任務後釋放鎖,任務在安全檢查點檢查 token,完成必要的交易收尾後返回。例外在入口捕獲並記錄。」
「關閉時先拒絕新任務,再設定 closed、notifyall、requeststop,最後 join。解構不會 detach,也不會提前釋放佇列或記錄器。測試涵蓋空佇列、出隊競態、例外、重複停止和長任務超時。」
常見錯誤
- 把 stop_token 當強制殺執行緒 → 破壞資源和交易 → 定義協作檢查點。
- 使用
std::thread後忘記 join → 解構觸發終止或執行緒懸掛 → 使用 jthread 或明確管理生命週期。 - 條件變數只等待一次通知 → 丟通知後永久睡眠 → 用謂詞迴圈並讓 stop 喚醒。
- 任務持鎖執行 → 停止和生產者被阻塞 → 出隊後釋放鎖。
- 例外穿出執行緒入口 → 程式終止 → 在線程邊界捕獲並記錄策略。
- 解構先釋放成員再停執行緒 → use-after-free → 先 stop、join,再釋放狀態。
追問及應對
jthread 解構會做什麼?
若仍可 join,解構會請求停止並 join;它不會強制終止任務。任務必須回應 stop,join 可能等待其到達安全檢查點。
stop 請求能喚醒 condition_variable 嗎?
使用支援 stop token 的 conditionvariableany 等待時,停止請求會使等待返回;自訂等待則需用 stop_callback 通知。
正在執行的資料庫寫入能立即取消嗎?
不能假設。應把寫入拆成可回滾或冪等步驟,等待驅動超時/取消能力,在提交邊界檢查 stop。
如何避免停止競態?
把 closed、佇列和 stop 視為同一生命週期協定,所有狀態轉換在鎖下完成,通知放在狀態改變之後;用壓力測試和 ThreadSanitizer 覆蓋停止與出隊同時發生的窗口。