題干與適用場景
一個非同步服務在快取命中時經常不需真正 I/O,卻仍為每個協程建立任務並等待事件迴圈排程。團隊想全域設定 asyncio.eagertaskfactory 來減少排程開銷。請判斷收益是否足以承擔語意變化,並提出安全的灰度方案。
Python 文件說明:啟用後,協程在 Task 建構期間同步開始;只有遇到阻塞才排入事件迴圈。此行為自 Python 3.12 提供,且會改變任務執行順序。
面試官考察點
觀察候選人能否區分「同步完成」與「非同步阻塞」,解釋 eager 執行對公平性、例外傳播、取消和 TaskGroup 的影響;是否只在量測證明收益的邊界啟用,保留版本檢查、回滾與指標。
回答前需要釐清的問題
- 服務最低 Python 版本及所有部署環境是否一致?
- 協程是讀本地快取,還是可能觸發網路、資料庫或鎖等待?
- 是否依賴任務建立順序、事件迴圈公平性或
call_soon回呼順序? - 失敗、取消與逾時是否由
TaskGroup或請求作用域統一管理? - 優化目標是 CPU 排程開銷、尾延遲還是吞吐?基線數據是什麼?
30 秒回答框架
「我不會直接全域開啟。eager factory 讓協程在建立 Task 時同步執行,快取命中可省掉一次事件迴圈排程;遇到阻塞才排隊。它會改變執行順序,可能讓建立任務的迴圈長時間佔用目前任務,也會改變例外何時暴露。先按 Python 版本和呼叫點灰度,量測命中率、事件迴圈延遲、尾延遲和錯誤率;若收益不足或公平性受損就回復預設 factory。」
分步深入解答
第一步:建立預設與 eager 的執行模型
預設 create_task 會把協程安排到事件迴圈稍後執行;eager factory 在建構期間立即推進協程,直到返回、拋錯或第一次阻塞。同步完成的任務可能根本不進入排程佇列。
loop.set_task_factory(asyncio.eager_task_factory)
task = asyncio.create_task(read_cached(key))所以不能把 eager 當成「更快但相同的排程器」;它是可觀察的執行語意變化。
第二步:定位適合的協程邊界
適合候選通常是純記憶體快取、記憶化計算和高命中率的短協程。會存取網路、資料庫、檔案、鎖或無界 CPU 的協程,應保持短暫且可阻塞的路徑,避免在建構 Task 時搶佔目前執行流。
第三步:分析順序與公平性
多個任務在迴圈中建立時,eager 協程可能按建立順序立即完成,改變原本交錯的結果順序。大量同步命中也可能讓目前任務連續執行很久,延後計時器、I/O 回呼和其他請求。應監控 event-loop lag,並限制單次批量建立量。
第四步:處理例外與取消
同步拋出的例外可能在 create_task 附近暴露,呼叫堆疊和捕獲位置與預設排程不同。呼叫方仍須保存 Task 參照,並讓 CancelledError 繼續傳播。不要用 eager 掩蓋請求取消或把例外轉成成功結果。
第五步:與 TaskGroup 和逾時組合
TaskGroup 仍負責任務樹、兄弟取消和 join,但組內任務可能在 create_task 時已完成或失敗。用結構化的 try/except* 處理例外,並在外層設定共享逾時;不要假設所有任務都先排隊再同時開始。
async with asyncio.TaskGroup() as group:
user = group.create_task(read_cached("user"))
orders = group.create_task(read_remote("orders"))第六步:版本與局部啟用
此 API 自 Python 3.12 提供,Python 3.14 的 createtask 也支援 eagerstart 參數。多版本服務應在啟動檢查版本,優先用局部任務或獨立事件迴圈驗證,而非無條件修改全域 factory。
第七步:設計灰度、回滾與指標
先在快取專用呼叫點啟用,設定可關閉開關;比較預設與 eager 的 CPU、任務建立耗時、快取命中尾延遲、event-loop lag、例外率、取消率與下游 QPS。發現公平性或錯誤回歸時恢復預設 factory,不需改寫任務業務邏輯。
第八步:驗證語意而非只測基準
測試同步返回、同步拋錯、第一次 await、外部取消、TaskGroup 多錯誤、逾時、遞迴建立任務、計時器公平性和混合快取/遠端批次。用事件記錄斷言順序,避免只跑吞吐基準就宣布優化成功。
高品質示範回答
「eager factory 適合高命中率、短而確定的快取協程,因為它省下一次事件迴圈排程;不適合不可預測的 I/O 或長 CPU 路徑。最大風險是同步執行改變順序、公平性和例外暴露時機。我的方案是 Python 版本檢查、局部灰度、保留預設回退,並觀測 loop lag、尾延遲、取消與錯誤指標。TaskGroup 與共享逾時仍保留,所有取消和資源清理照舊。」
常見錯誤
- 把 eager 當成無語意變化的效能開關 → 順序和例外時機改變 → 先寫出兩種執行模型並量測。
- 對所有協程全域啟用 → 慢 I/O 或 CPU 阻塞目前任務 → 只在短同步路徑灰度。
- 忽略 Python 版本 → 舊環境啟動失敗 → 啟動時檢查版本並保留預設 factory。
- 只看平均吞吐 → 事件迴圈公平性退化 → 加入 loop lag 和尾延遲指標。
- 認為 TaskGroup 行為完全不變 → 同步例外捕獲位置錯誤 → 覆蓋建構階段失敗與多例外測試。
- 吞掉取消或清理例外 → 請求結束後仍有洩漏 → 保留取消傳播並驗證資源釋放。
追問及應對
追問一:快取協程同步返回時,Task 還會存在嗎?
可能在建立階段已經完成;不要依賴它一定經歷事件迴圈排程。呼叫方仍應使用返回物件讀取結果,並覆蓋同步完成路徑。
追問二:eager 是否保證任務按建立順序完成?
不保證業務層的全域順序。它只改變開始時機;遇到阻塞後仍受事件迴圈排程,多個任務的完成順序必須透過明確協調或結果排序保證。
追問三:如何避免一批快取命中餓死其他請求?
限制批量大小,必要時在批次間主動讓出控制權,並監控 loop lag;也可只對單一低成本呼叫點使用 eager。
追問四:發生回歸如何回滾?
關閉開關並恢復預設任務工廠,確認指標回到基線,再保留帶事件順序和版本資訊的樣本定位。
追問五:eager_start 與 factory 有何關係?
eager_start 是建立單一 Task 時的明確選項;factory 是事件迴圈級預設策略。兩者都必須按實際 Python 版本確認簽名和優先級,不能混用舊版本假設。