C++ 程式設計面試:如何用 C++26 std::execution 組合可取消的非同步流水線?
題干與適用場景
服務要把讀取、轉換與彙總組合成非同步流水線。呼叫方可能因逾時、用戶端斷線或資源不足而取消;每個階段也可能失敗,部分工作已經在執行。請使用 C++26 std::execution 的 sender/receiver 模型,說明排程、完成訊號、生命週期與降級路徑。
這題考察並行抽象的邊界,不要求背誦某個函式庫語法。標準執行控制函式庫把 sender 描述的工作圖與 receiver 的完成處理分開,並用 operation state 保存連線後的非同步狀態。答案應把可取消性、錯誤語意和資源回收寫成契約。
面試官考察點
- 能否區分惰性 sender、連線後的 operation state 和真正開始執行的
start。 - 能否使用 scheduler 表達執行資源,而不是在業務函式中任意建立執行緒。
- 能否分別處理
setvalue、seterror和set_stopped,避免把取消偽裝成例外。 - 能否讓取消訊號穿過每個階段,並保證停止後不再提交新的副作用。
- 能否說明平行階段的背壓、限額、例外安全與結果彙總。
- 能否提供標準庫不可用時的相容層、能力偵測與一致性測試。
回答前需要澄清的問題
- 讀取是本地檔案、網路請求還是資料庫游標?每個階段是否可重複或有外部副作用?
- 取消是盡快停止、在目前不可中斷呼叫後停止,還是必須回滾已提交的結果?
- 平行度、記憶體上限、單項逾時與整體截止時間分別是多少?
- 彙總是否要求輸入順序、穩定浮點結果或部分結果可見?
- 目標編譯器和標準庫是否實作 C++26 execution,還是只能使用實驗性實作?
30 秒回答框架
我會先定義流水線的完成契約:值、錯誤、停止三條通道,取消只阻止尚未開始的工作,並讓可中斷階段盡快回應。每個 sender 保持惰性,連線後取得 operation state,只有呼叫 start 才開始。使用明確 scheduler 管理執行資源,以平行度與佇列上限保護記憶體。彙總器規定順序與部分結果規則;工具鏈透過能力偵測選擇標準實作、相容函式庫或同步純量路徑,三條路徑共享同一組取消、錯誤與結果測試。
分步驟深入解答
1. 先畫出惰性工作圖
把讀取 sender 接到轉換 sender,再接彙總 sender。then 適合把已產生的值傳給下一步,letvalue 適合依結果建立下一段非同步工作,whenall 適合平行分支。組合只建立工作圖,不應在建立階段執行 I/O。
2. 明確連線與生命週期
sender 與 receiver 透過 connect 產生 operation state;呼叫 start 後才允許執行。operation state 的位址必須在非同步操作結束前保持有效,因此不能放在即將返回的堆疊框架中。擁有者應把狀態放在請求內容或非同步作用域裡,並在完成、錯誤和停止三條路徑都釋放資源。
3. 把資源交給 scheduler
scheduler 是執行資源的輕量控制代碼。讀取可放在 I/O 資源,CPU 轉換放在受限的平行資源,on、startson 或 continueson 表達階段邊界。不要在每個元素裡建立執行緒;用固定平行度、佇列長度與批次大小限制記憶體和上下文切換。
4. 傳播停止、錯誤和值
值完成進入下一階段,錯誤進入統一錯誤處理,停止進入取消處理。receiver 環境中的 stop token 是取消觀察點;阻塞系統呼叫必須有可中斷等待或有限逾時,否則只能在呼叫返回後回應停止。取消不等於回滾,若階段已寫入外部系統,應使用冪等鍵、補償動作或明確的不可撤銷邊界。
5. 最小組合示意
下面程式展示工作圖形狀;實際讀取與執行緒池 sender 需要由專案提供。
using namespace std::execution;
auto pipeline = read_sender()
| let_value([](Batch batch) {
return bulk_transform(batch, get_parallel_scheduler());
})
| then([](Transformed value) { return summarize(value); })
| upon_error([](std::exception_ptr error) { record_failure(error); })
| upon_stopped([] { record_cancellation(); });
auto state = connect(std::move(pipeline), receiver);
start(state);receiver 必須由仍然存活的請求作用域擁有,並在自己的環境中提供停止令牌。生產程式還應記錄階段、批次、截止時間與取消原因,避免只看到泛化錯誤。
6. 平行彙總與副作用邊界
平行轉換應保持每個任務的區域狀態,彙總階段再按定義的順序合併。若結果允許無序合併,要說明非結合浮點運算帶來的差異;若要求穩定結果,保留索引或分區序號。寫入外部系統前先檢查停止令牌,提交後記錄冪等鍵,不能把 set_stopped 當成已撤銷提交。
7. 降級、測試與觀測
用 feature-test 巨集、編譯器版本和標準庫能力建立矩陣。標準 execution 不可用時,相容實作可以提供相同的內部 sender 契約;再不行就使用受限執行緒池或同步路徑,但保持值、錯誤、停止語意一致。測試空輸入、部分批次、重複取消、錯誤與取消競速、資源耗盡、operation state 提前銷毀和多次啟動。基準記錄吞吐、尾端延遲、佇列長度、取消回應時間與未完成任務數。
高品質示範回答
我會把流水線設計成惰性 sender 圖:讀取、平行轉換、彙總分別定義完成簽名,再透過 connect 產生 operation state,只有 start 才執行。I/O 與 CPU 使用不同 scheduler,並用平行度、佇列與批次大小限制資源。receiver 處理值、錯誤與停止三條通道;停止令牌在每個可中斷點檢查,外部寫入使用冪等鍵和補償邊界,取消不承諾回滾。
operation state 由請求作用域持有到完成,錯誤和停止都走統一清理。工具鏈先偵測 C++26 execution,選擇標準實作、相容實作或同步降級;三條路徑共享行為測試,涵蓋空批次、錯誤競速、取消回應、資源耗盡和提前銷毀。上線後觀察尾端延遲、佇列長度、取消回應和洩漏,確認平行化確實改善目標指標。
常見錯誤
- 把建立 sender 當成已啟動非同步任務。
- 讓 operation state 在函式返回後失效。
- 只使用例外通道,把取消當成普通錯誤。
- 每個元素建立執行緒,忽略佇列、平行度和記憶體上限。
- 看到停止訊號就聲稱外部副作用已經回滾。
- 平行彙總沒有定義順序、浮點誤差或部分結果規則。
- 只寫一條標準庫路徑,未準備能力偵測和相容降級。
追問及應對
sender 何時真正執行?
組合 sender 只描述工作圖;連線產生 operation state,呼叫 start 後才啟動非同步操作。測試應分別涵蓋建立、連線和啟動階段。
停止訊號能強制終止系統呼叫嗎?
不能保證。系統呼叫必須提供可中斷介面、逾時或分塊檢查;否則只能在目前呼叫返回後回應停止,並記錄最壞回應時間。
錯誤與停止同時發生怎麼辦?
定義優先級與一次性完成規則,保證 receiver 只收到一個最終完成訊號。記錄原始錯誤與停止原因,避免把競速隱藏成成功。
when_all 中一個分支失敗會怎樣?
明確其他分支是繼續、請求停止或等待清理;共享資源要有作用域與取消傳播,彙總器不能讀取已失效的分支結果。
如何保證彙總結果可重複?
保留分區序號並按固定順序合併,或明確允許無序結果及誤差範圍。浮點運算不滿足結合律時不能只依賴平行歸約。
標準庫還不支援 C++26 怎麼辦?
透過編譯能力矩陣選擇相容實作或同步路徑,保持內部完成語意與測試不變。不要讓公共介面暴露某個實驗庫的私有型別。