Watch at Ease看見世界,讀懂生活。

科技 · 實用指南

Python 出現 cannot schedule new futures after shutdown?先證明哪個生命週期先結束

先區分執行器實例已關閉、直譯器退出與 asyncio 預設執行器收尾,再決定重試、替換或調整容量。

原創圖示:從 Python future 提交失敗追蹤生命週期所有者與安全收尾
原創示意圖:重試、替換或擴充執行器前,先證明哪個生命週期已結束。 Watch at Ease

RuntimeError: cannot schedule new futures after shutdown 不是要求增加工作執行緒。它表示提交者跨過生命週期邊界:某個 Executor 已關閉、asyncio 預設執行器已收尾,或直譯器已開始退出。精確後綴與完整 submit 呼叫堆疊會指出真正的所有者。

增加重試前先保存證據。安全修復讓每一代執行器只有一位所有者:先停止新生產者,再明確等待或取消已接受工作,最後只呼叫一次 shutdown。若程序退出,必要控制執行緒不能只靠 daemon;若 asyncio.run 擁有迴圈,也應讓它管理預設執行器。

1. 分類精確的關閉訊號

保存完整回溯,記錄訊息是 after shutdown 還是 after interpreter shutdown,並確認呼叫來自 submit、map、排程器、函式庫助手或 run_in_executor。

在建立、每次接受提交、開始關閉、關閉完成及首次拒絕時記錄執行器世代、程序號、執行緒或任務名與單調時間。修復依賴公開所有權與順序契約。

2. 修復實例關閉後的重用

with 區塊退出時會關閉執行器。長期物件若保存該實例,第一次可成功,第二次就失敗。每次獨立執行可建立新執行器,或把 with 移到真正擁有完整重用生命週期的元件。

不要讓每個呼叫者捕捉 RuntimeError 後自行建立新池。所有者應以鎖或狀態機公布目前世代,進入 draining 後拒絕新工作,並在舊世代核清後才替換。

3. 修復直譯器退出與 daemon 競態

interpreter shutdown 表示程序生命週期正在結束。常見原因是 main 已返回,但 daemon 控制器、排程器、回呼或傳輸助手仍準備提交工作;主執行緒未處理例外也可能觸發。

使用明確停止訊號:停生產者、禁止新提交、等待控制執行緒、結算 futures、關閉執行器,最後才讓 main 返回。必要工作不能只有 daemon 所有者,atexit 也無法復活已收尾的執行器。

4. 尊重 asyncio 預設執行器所有權

run_in_executor(None, ...) 使用迴圈的預設執行器。shutdown_default_executor() 後再提交會報錯。使用 asyncio.run 時,runner 已管理生成器、預設執行器與迴圈關閉,不應重複收尾。

手動管理迴圈時,先停止會產生 executor 工作的回呼與服務,等待相關任務,再 await shutdown_default_executor(),最後關閉迴圈。跨執行緒回呼必須在邊界前拒絕或排空。

5. 以重啟與退出情境證明修復

至少測試連續兩次正常執行、排隊中關閉、生產者活躍時重啟、主執行緒失敗。每個已接受工作只能有一個終態,draining 後不得提交,執行緒數要回到基線。

先定義待處理工作要完成或取消,再明確斷言。驗收證據應顯示生產者停止早於 executor 關閉,且壓力測試沒有重複副作用、孤兒執行緒、吞掉例外或延遲提交。

原創圖示:Python executor 實例、直譯器與 asyncio 預設執行器關閉矩陣
原創示意圖:依精確 RuntimeError 路徑選擇所有權相符的修復。 Watch at Ease