題目與背景
C++26 將 std::execution sender/receiver 模型納入標準化方向。給定讀取網路區塊、解析記錄、批次寫盤三個階段,請組合成 sender pipeline。呼叫方可以請求停止,也可以把不同階段放到 I/O 或 CPU 排程器;要求說明值、錯誤、停止三條完成通道與資源清理。
面試官考察什麼
重點是區分惰性的 sender 描述與連接後的 operation state,理解 connect 只建立狀態而 start 才啟動工作。答案還要說明 receiver 的 setvalue、seterror、set_stopped 如何沿管道傳播,排程器切換是否改變執行緒親和性,以及取消時如何避免提交新的副作用。
先問清楚的澄清問題
任務與背壓
確認每批大小、允許的並行數、是否必須保持輸入順序,以及寫盤失敗後是否允許重試。背壓策略決定是限制 sender 並發,還是由佇列容量施加上限。
排程與停止來源
確認 I/O 與 CPU scheduler 的介面、停止請求來自逾時還是使用者操作,以及停止後已提交的系統呼叫如何收尾。停止請求不等於強制結束執行緒。
資源與提交語義
確認檔案控制代碼、緩衝區與暫存檔的所有權,寫入是否冪等,部分批次是否需要回滾。這樣才能界定 set_error 後允許的清理動作。
30 秒回答框架
「我先用 sender adaptor 描述讀取、解析和寫入,再用排程器 adaptor 切換執行內容。pipeline 保持惰性,connect 產生 operation state,start 才開始。每個階段把成功值交給下游,例外走 seterror,停止請求走 setstopped;共享 stop token 貫穿所有階段。停止路徑在提交副作用前檢查 token,已提交的 I/O 由擁有者完成關閉和暫存檔清理。」
深入解答步驟
第一步:定義值和錯誤邊界
為每個階段定義輸入輸出型別,並把可恢復錯誤轉換為明確的 error sender。不要讓例外跨越 scheduler 邊界;最終 receiver 統一記錄成功、失敗或停止。
第二步:組合惰性 pipeline
用 let_value、then 或等價 adaptor 連接讀取、解析、寫入。組合階段只建構描述,不分配執行緒,也不執行 I/O;需要共享狀態時讓 operation state 持有生命週期。
第三步:連接並啟動
呼叫 connect(sender, receiver) 得到 operation state,把它保存於仍存活的作用域,再呼叫 start。receiver 必須比 operation state 活得久,非同步回呼不能引用已銷毀的堆疊物件。
第四步:切換執行內容
在 I/O 完成後用 scheduler sender 把解析階段移到 CPU 執行緒池,寫盤階段再回到受控的 I/O 執行緒。記錄 scheduler 的佇列容量和公平策略,避免把阻塞寫入放進無限制的通用池。
第五步:傳播停止和背壓
將 stop token 傳給每個可中斷階段。收到停止後,新的批次不再入列,已執行的系統呼叫按 API 能力取消或等待完成;佇列滿時透過限流 sender 暫停上游,防止記憶體無界增長。
第六步:處理錯誤和部分副作用
寫盤前先寫暫存檔或記錄批次序號,成功後原子提交。set_error 觸發下游清理並關閉控制代碼;重試必須有上限和冪等鍵,避免重複寫入。
第七步:驗證並行與生命週期
測試多 scheduler、停止競態、解析例外、寫盤短寫和 receiver 提前銷毀。用執行緒分析器檢查資料競爭,統計排隊時間、吞吐、停止延遲和未釋放 operation state 數量。
高品質示例回答
我會讓讀取 sender 產生批次,解析 sender 在 CPU scheduler 執行,寫入 sender 在 I/O scheduler 執行。pipeline 只描述依賴;connect 建立 operation state,start 才啟動。每階段都實作值、錯誤、停止三條路徑,並共享 stop token。停止時阻止新批次入列,已提交 I/O 完成關閉;寫入使用暫存檔和批次序號保證重試冪等。測試涵蓋 scheduler 切換、背壓、停止競態和 receiver 生命週期。
常見錯誤
- 錯誤: 建構 sender 就認為任務已執行。→ 原因: sender 是惰性描述。→ 改進: 明確 connect 和 start 的啟動邊界。
- 錯誤: 只處理例外,不處理停止完成訊號。→ 原因: 停止是獨立完成通道。→ 改進: receiver 同時實作 seterror 與 setstopped。
- 錯誤: 停止時直接殺執行緒。→ 原因: 執行緒可能持有檔案控制代碼或半寫入狀態。→ 改進: 傳播 stop token,按階段安全收尾。
- 錯誤: 無界地把批次提交到執行緒池。→ 原因: 缺少背壓會耗盡記憶體。→ 改進: 限制並行、佇列容量和重試次數。
追問與回答
追問 1:為什麼不直接用 future?
sender/receiver 把排程、取消和三種完成通道作為可組合結構,能在連接時統一處理生命週期;future 通常需要額外約定停止和錯誤傳播。
追問 2:start 呼叫後能否立即銷毀 sender?
可以銷毀臨時 sender 描述,但 operation state、receiver 及其捕獲的資源必須保持有效,直到收到完成訊號。實際所有權應由任務物件或作用域管理。
追問 3:set_stopped 是否代表所有副作用都撤銷?
不代表。它表示計算以停止完成;已提交的 I/O 可能無法回滾,必須依靠暫存檔、冪等提交或補償操作保持一致性。
追問 4:怎樣證明不會出現重複寫入?
為每批產生穩定序號,提交前檢查已完成記錄,重試只允許寫入缺失序號。測試故障注入、程序重啟和停止競態,驗證提交日誌與最終檔案一致。