題幹與適用場景
面試官給出一個請求處理函式,需要並行執行三個彼此相關的子任務:讀取使用者資料、讀取訂單與產生推薦。用戶中斷連線、任一關鍵子任務失敗或總截止時間到達時,系統不能留下繼續執行的孤兒任務。回答要解釋結構化並行的核心思想,再說明怎樣把它落到不同語言的任務群組、協程作用域或結構化任務範圍中。
這道題適合中高級後端、平台、行動端與跨語言基礎設施職位。重點不是背某個 API,而是能否把任務的進入點、退出點、失敗策略與資源歸屬說清楚。
面試官考察點
面試官會看你是否把並行任務視為父任務的一部分,而不是把工作丟進全域執行器就返回。強回答會說明子任務不能越過父作用域存活,父任務取消如何遞迴傳播,失敗是快速失敗還是允許部分結果,以及如何保證 join、關閉與指標記錄都發生在邊界內。
普通回答只說「用 async/await 並行執行」。高品質回答會把結構化並行與非結構化 Future、全域背景任務區分開,明確哪些工作屬於請求生命週期,哪些工作應該轉成獨立佇列或持久化工作流。
回答前需要釐清的問題
- 三個子任務是否都必須成功?使用者資料與訂單失敗時,推薦是否仍有價值?
- 截止時間是用戶請求的硬 deadline,還是服務端可以繼續完成的軟預算?
- 子任務是否只做可取消的 I/O,還是包含不可中斷的外部副作用?
- 用戶中斷連線後,是否允許把結果轉入非同步通知或背景任務?
- 失敗時需要回傳一個錯誤、部分結果,還是可解釋的降級狀態?
這些答案會改變作用域邊界:請求內的短任務留在父作用域,跨請求繼續執行的工作必須明確轉移所有權,不能偷偷逃逸。
30 秒回答框架
我先把一次請求定義為父任務,三個並行讀取是它的子任務;父任務只有在子任務完成、失敗或取消處理完後才能退出。關鍵資料失敗時快速取消兄弟任務,推薦失敗則回傳明確的降級結果。用戶中斷連線與 deadline 都向下傳播取消訊號,子任務在 finally 或等價清理區塊釋放連線與句柄。若工作必須在請求結束後繼續,我會把它寫入持久佇列,建立新的生命週期,而不是讓背景 Future 脫離作用域。最後用取消、逾時、部分失敗與洩漏指標做故障測試。
分步驟深入解答
第一步:先畫出任務樹
把請求處理函式作為根節點,資料、訂單與推薦是它的直接子節點。子任務繼承父任務的 deadline、追蹤上下文與取消訊號。父任務負責等待結果、合併錯誤並關閉作用域;子任務不能把未完成的工作交給全域執行器後就讓父函式返回。
這棵樹提供一個可檢查的不變量:父任務退出時,所有子任務都已完成、取消或被明確轉移給另一個有記錄的擁有者。沒有這個不變量,執行緒洩漏、重複寫入與不可解釋的尾延遲都會被隱藏。
第二步:選擇失敗與部分結果策略
先按業務價值分類。使用者資料與訂單是結算頁面的必要資料,任一失敗就取消兄弟任務並回傳可重試錯誤;推薦是增強資訊,失敗時回傳沒有推薦的回應。不要讓低價值任務的失敗拖垮整個請求,也不要把關鍵資料的缺失偽裝成空物件。
虛擬碼可以表達策略,但不綁定語言:
within request_scope(deadline):
profile = child(fetch_profile)
orders = child(fetch_orders)
recommendations = child(fetch_recommendations)
wait(profile, orders)
if profile.failed or orders.failed:
cancel_all_children()
return retryable_error
return compose(profile, orders, recommendations.or_empty)第三步:讓取消真正到達資源邊界
取消不是把布林值寫進任務物件就結束。網路客戶端、資料庫驅動與檔案讀取都要檢查取消訊號;等待操作需要可中斷;重試迴圈必須在下一次嘗試前重新檢查 deadline。對於無法撤銷的外部副作用,例如已提交的付款或已寄出的郵件,取消只能阻止後續步驟,不能聲稱已回滾既有事實。
用戶中斷連線時,入口層取消根任務。根任務再向子任務傳播,子任務在清理區塊中關閉回應體、連線、訂閱與暫存檔案。清理動作本身也要有上限,避免「等待清理」永遠阻塞請求收尾。
第四步:區分逾時、取消與失敗
逾時是預算耗盡,取消是上游明確不再需要結果,失敗是任務無法完成。三者可能同時出現,但日誌與用戶狀態不能混成一個 500。記錄原始原因、觸發者與任務路徑;對用戶中斷通常不重試,對下游逾時可按剩餘預算決定是否一次短重試,對業務錯誤則交給明確的降級策略。
不要把 sleep 或盲目重試放在作用域外。它們會消耗已過期的預算,並在父任務返回後繼續製造負載。
第五步:驗證結構是否真的被保持
測試父任務正常完成、關鍵子任務失敗、可選子任務失敗、父任務取消、deadline 到期與子任務啟動前取消。每次測試都檢查:活動子任務最終回到零;下游連線關閉;追蹤中能看到父子關係;沒有重複副作用;錯誤分類與用戶狀態一致。
對執行時指標記錄活動任務數、取消傳播延遲、deadline 逾時率、子任務例外、清理耗時、被取消後仍執行的任務數與每個請求的最大扇出。只有「請求回傳成功」而沒有這些指標,不能證明結構化並行生效。
高品質示範回答
「我會把一次請求當作父任務,把三個讀取操作放進同一個受 deadline 約束的作用域。父任務負責建立、等待與關閉子任務,子任務的生命週期不能超過這個作用域。使用者資料與訂單是關鍵依賴,任一失敗就取消兄弟任務並回傳可重試錯誤;推薦是可選依賴,失敗時回傳明確的空推薦狀態,同時記錄降級原因。
用戶中斷連線、父任務逾時或上游取消都會沿任務樹向下傳播。每個 I/O 呼叫都使用可取消介面,重試前檢查剩餘預算,清理區塊關閉連線與訂閱。已發生的外部副作用不假裝可以回滾;如果必須在請求結束後繼續,我會先寫入持久佇列,再由消費者開啟新的任務樹。
我會用故障注入驗證關鍵失敗、可選失敗、取消競態與清理逾時,並觀察活動任務、取消延遲、洩漏數與父子追蹤。這樣回答的重點是生命週期與所有權,不依賴 Java、Kotlin 或 Swift 的某個具體 API。」
常見錯誤
- 把結構化並行等同於並行執行 → 只描述同時啟動任務,遺漏父子生命週期 → 畫出作用域、join、取消與關閉邊界。
- 用全域執行器提交後立即返回 → 父請求結束後任務仍可能存取已失效資源 → 讓任務留在請求作用域,或明確轉移到持久佇列。
- 把取消當成強制殺死 → 外部副作用可能已經完成,資源也未必自動釋放 → 聲明合作式取消、不可撤銷操作與清理責任。
- 所有失敗都回傳空結果 → 關鍵資料缺失被偽裝,呼叫方無法區分降級與錯誤 → 按業務關鍵性定義快速失敗與部分結果。
- 只測成功路徑 → 取消競態與孤兒任務不會暴露 → 注入中斷、逾時、兄弟失敗與重複取消,並檢查活動任務最終歸零。
追問及應對
追問一:如果推薦產生需要幾秒,用戶請求已經結束,怎麼辦?
先確認用戶是否仍需要這項結果。若頁面只在當前請求展示,推薦必須隨父任務取消;若業務要非同步產生,先把輸入與冪等鍵寫入持久佇列,再由消費者建立新的作用域。新的消費者擁有獨立 deadline、重試與告警,不能借用已結束的請求上下文。
追問二:一個子任務失敗,為什麼不能讓其他任務繼續跑完再決定?
可以,但要先證明剩餘工作有價值且不會超過預算。關鍵資料失敗後繼續查詢訂單通常只會浪費連線與下游容量;對可獨立快取的資料或審計證據,繼續完成可能合理。回答應給出取消策略的條件,而不是把快速失敗當成永遠正確。
追問三:取消訊號已經發出,但資料庫查詢仍在執行,怎麼處理?
檢查驅動是否支援取消與連線歸還;驅動不支援時設定獨立的查詢逾時、隔離連線池與結果丟棄保護,避免把取消延遲偽裝成已停止。記錄從取消到資源釋放的時間,超過門檻就降級、隔離或告警。不可中斷的查詢不能繼續占用請求級配額。
追問四:結構化並行是否取代所有執行緒池與訊息佇列?
不取代。它適合有明確父子關係、需要共同取消與共同收尾的短生命週期工作。跨請求任務、定時任務、需要持久重試的工作仍應使用佇列或工作流;共享執行緒池可以作為執行資源,但提交任務的作用域必須定義誰等待、誰取消與誰負責結果。