具代表性的面試主題

C++ 面試題:用 std::jthread 和 stop_token 實作可協作取消

程式題困難
Offer.cc 編輯團隊發佈 更新

題幹

請用 C++20 實作一個 worker:它循環消費任務,主執行緒可以請求停止;停止時不能遺失已開始任務、不能永久阻塞,也不能在物件解構後存取共享狀態。請說明 std::jthread、stop_token、condition_variable_any、例外和測試策略。

題幹與適用場景

你需要實作一個背景 worker,持續從佇列讀取任務。服務關閉或上游取消時,主執行緒請求 worker 停止;已開始的任務要完成,尚未開始的任務可以保留或明確丟棄。實作必須避免忙等、資料競爭、解構時遺留執行緒和條件變數永久等待。

C++20 的 std::jthread 會在解構時請求停止並 join;它可以向入口函式注入 std::stop_token。停止請求是協作訊號,不會強制終止任意程式碼;worker 必須輪詢或使用可回應 stop token 的等待。

面試官考察點

  • 是否理解 stop token 是共享狀態上的請求,不是非同步殺執行緒。
  • 是否使用 std::jthread 的自動 join,並避免執行緒仍存取物件成員時解構物件。
  • 是否讓阻塞等待能被停止喚醒,例如 condition_variable_any::wait 的 stop-token 多載。
  • 是否保證佇列、停止狀態、任務例外和資源清理的生命週期安全。
  • 是否測試停止競態、空佇列、任務拋例外、重複 request_stop 和解構順序。

回答前需要釐清的問題

  • 停止時正在執行的任務能否完成,任務是否具冪等性或有外部副作用?
  • 佇列關閉後,未消費任務是丟棄、轉移還是等待另一個 worker?
  • worker 的等待是否支援 condition_variable_any,還是依賴不可中斷的第三方 I/O?
  • 例外由 worker 記錄、傳給 join 方,還是觸發整體關閉?
  • 物件是否可能由多個執行緒呼叫 stop、解構或重新啟動?

30 秒回答框架

「用 std::jthread 持有 worker,讓入口接收 std::stop_token。佇列用互斥鎖保護,等待時使用能回應 token 的 condition_variable_any;喚醒後先判斷 stop、佇列關閉和任務狀態。已取出的任務執行到明確的取消檢查點,再釋放資源。解構依靠 jthread 的 request_stop 加 join,禁止 detach。所有共享狀態的生命週期覆蓋執行緒,並測試停止競態和例外。」

分步驟深入解答

第一步:定義取消契約

把停止分為「請求停止」和「任務完成」。stop 請求只阻止 worker 取得新任務;已開始任務在安全檢查點完成或回傳可識別的取消結果。不能承諾任意第三方呼叫會立即終止。

第二步:設計執行緒與狀態所有權

讓擁有佇列、互斥鎖、條件變數和 std::jthread 的物件活得比執行緒久。解構先請求停止並等待,再釋放成員。禁止捕獲已離開作用域的引用。

第三步:讓阻塞等待可取消

使用 condition_variable_any 的 stop-token 等待,或註冊 stop_callback 呼叫 notify_all。謂詞同時檢查佇列非空、佇列關閉和 stop_requested();被喚醒後重新持鎖判斷。

cpp
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 的 condition_variable_any 等待時,停止請求會使等待返回;自訂等待則需用 stop_callback 通知。

正在執行的資料庫寫入能立即取消嗎?

不能假設。應把寫入拆成可回滾或冪等步驟,等待驅動超時/取消能力,在提交邊界檢查 stop。

如何避免停止競態?

把 closed、佇列和 stop 視為同一生命週期協定,所有狀態轉換在鎖下完成,通知放在狀態改變之後;用壓力測試和 ThreadSanitizer 覆蓋停止與出隊同時發生的窗口。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

截圖題目後,依序看約束、解法、程式碼、邊界條件和複雜度。

查看工具