具代表性的面試主題

程式設計面試:如何保證 Rust Tokio 非同步程式碼的取消安全?

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

題幹

在 Tokio 中用 `select!` 等待逾時、取消令牌和 I/O 時,如何判斷一個 future 是否 cancellation safe?如果操作已部分完成又被取消,你會如何恢復?

題幹與適用場景

在 Tokio 中用 select! 等待逾時、取消令牌和 I/O 時,如何判斷一個 future 是否 cancellation safe?如果操作已部分完成又被取消,你會如何恢復?

這道題適合 Rust、後端、基礎設施和非同步服務職位。Tokio 文件把取消描述為丟棄 future;select! 勝出的分支繼續,其他分支可能在任意 await 點被丟棄。關鍵是區分「停止等待」與「撤銷外部副作用」,並讓狀態機可安全重啟。

面試官考察點

  • 是否理解 future 被 drop 不等於底層 I/O 或遠端副作用已回滾。
  • 是否能辨識讀取緩衝、佇列、鎖和協議框的部分進度。
  • 是否用所有權、狀態機和冪等鍵保證重啟不會遺失資料或重複提交。
  • 是否知道哪些 Tokio 原語明確聲明 cancellation safety,哪些需要自行封裝。
  • 是否區分 abort、逾時、優雅關閉和外部取消的語意。
  • 是否用模型測試、故障注入和指標驗證取消路徑。

30 秒回答框架

「我先定義取消邊界:丟棄 future 只停止目前任務的輪詢,不保證遠端操作撤銷。若一個 future 在任意 await 後被丟棄,再次呼叫仍能正確繼續或重試,才可稱為 cancellation safe。讀取和傳送採用明確狀態機,已消費位元組、請求 ID、緩衝區和冪等鍵都由擁有者保存;不可逆副作用先落持久狀態或等待確認。逾時和取消測試要覆蓋每個 await 點。」

分步驟深入解答

第一步:標記可取消點

審查 async 函式中的每個 .await:它前面是否已經改變了本地或遠端狀態?如果 future 在這裡被 drop,哪些欄位仍可用於恢復?把網路讀取、寫入、鎖等待、佇列接收和 sleep 都視為可能的取消點,而不是只測試函式入口和出口。

第二步:區分取消與撤銷

select! 取消一個分支通常只是丟棄 future;已經傳送到遠端的請求可能繼續執行。逾時處理器應記錄請求 ID 和未知結果狀態,不能直接把它標成失敗後再次傳送非冪等操作。需要撤銷時,必須呼叫協議提供的取消介面並等待確認。

第三步:設計可恢復的狀態機

把操作分成準備、傳送、等待確認、已提交和補償狀態。狀態與緩衝區由明確的擁有者保存;重新進入時根據狀態決定繼續讀取、重發、查詢結果或執行補償。對串流協議記錄框邊界和已確認偏移,避免從半個訊息重新解釋位元組。

第四步:保證佇列與鎖的一致性

從 channel 或 stream 取出訊息後,在處理結果落盤前被取消會造成「已消費但未完成」。使用交易式確認、可重複讀取或持久化租約;不要在 select! 分支中先彈出訊息再把唯一副本放進臨時變數。鎖保護的共享狀態要在 drop 時釋放,並讓恢復邏輯知道鎖內操作是否已提交。

第五步:處理資源與任務終止

JoinHandle::abort 會停止任務,但不取代業務清理。為 socket、檔案、暫存檔和 semaphore permit 設計 RAII 守衛;需要後台完成的工作交給獨立任務並保存句柄。優雅關閉先停止接收新任務,再等待可完成操作,最後對剩餘任務發取消訊號並記錄原因。

第六步:驗證取消安全

在每個 await 前後注入取消,檢查重啟、重複、遺失和資源洩漏。用受控 fake I/O、模型狀態機和並行測試覆蓋逾時、channel 關閉、遠端斷線與任務 abort。指標包括未決請求、重複鍵命中、補償次數、取消延遲和 permit 洩漏;沒有這些指標就無法判斷「取消成功」。

取捨、邊界與資訊增益

取消安全的核心資訊是:非同步控制流被中斷時,業務狀態仍必須可解釋。短小、無外部副作用的 future 容易做到重啟安全;跨網路、資料庫和訊息佇列的操作需要冪等、確認和補償。把所有程式碼包進不可取消任務會隱藏資源問題,也會破壞關閉延遲,因此應只在明確的原子邊界使用。

高品質示範回答

「我把每個 .await 當作取消點,先問它之前是否產生了副作用以及丟棄後如何恢復。Tokio 的 select! 只會丟棄未勝出的 future,不會撤銷已發出的請求,所以逾時要保留請求 ID,並讓重試使用冪等鍵或先查詢結果。操作狀態機至少區分準備、傳送、等待確認、已提交和補償。

channel 消費、檔案寫入和資料庫提交要有交易式確認或可重複讀取,避免訊息已取出卻沒有完成。資源用 RAII 守衛釋放,後台必須完成的工作由獨立任務持有句柄;優雅關閉先停止接收,再取消和等待。

測試在每個 await 點注入取消,覆蓋重啟、重複、遺失和洩漏,監控未決請求、重複鍵、補償、取消延遲和資源 permit。這樣我能證明 future 的 cancellation safety,而不是只看到任務停止。」

常見錯誤

  • 把 drop 當作遠端撤銷 → 請求可能已經執行 → 保存 ID、查詢結果或呼叫顯式取消。
  • 消費訊息後才考慮取消 → 唯一副本可能遺失 → 使用確認、租約或可重複讀取。
  • 逾時後盲目重發寫請求 → 產生重複副作用 → 使用冪等鍵或先查詢狀態。
  • 只在入口測試取消 → 中間 await 才有競態 → 對每個取消點注入測試。
  • 用 abort 取代清理 → 檔案、鎖和 permit 可能洩漏 → 使用守衛和關閉協議。
  • 所有任務都不可取消 → 優雅關閉會無限等待 → 只保護真正的原子邊界。

追問及應對

如何判斷一個 Tokio 原語是否 cancellation safe?

查看其文件是否明確聲明,在 select! 中被取消後再次呼叫不會遺失資料。沒有聲明時按可能遺失部分進度處理,自行保存狀態並寫恢復測試。

逾時後如何知道資料庫寫入是否完成?

使用客戶端請求 ID 和唯一約束查詢結果,或讓服務端提供可查詢的操作狀態。不要僅憑客戶端逾時就再次執行非冪等寫入。

JoinHandle::abort 之後可以立即刪除資源嗎?

只有確認任務已停止且資源的 drop 已完成才安全。等待 join 結果或使用擁有資源的守衛,不能把發出 abort 當作同步完成。

什麼時候應該把工作移到獨立任務?

當工作必須完成、需要跨請求重試或不能在目前取消邊界回滾時,交給持久佇列或獨立任務,並用句柄、狀態儲存和冪等鍵觀察它;不要用獨立任務逃避所有取消語意。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

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

查看工具