asyncio.run() 默认忽略 sigterm,需手动配置事件循环信号处理器并实现任务取消与资源清理。注册同步信号处理函数,调用 task.cancel() 后 await gather(return_exceptions=true),协程内用 try/except cancellederror 或 finally 释放资源,注意第三方 awaitable 及子进程、线程等边界处理。

asyncio.run() 无法捕获 SIGTERM?用 asyncio.run() 启动的服务默认忽略 SIGTERM
直接调用 asyncio.run(main()) 启动的程序,Python 会把 SIGTERM 当作普通中断信号处理——它不会触发协程取消,也不会等待任务清理,进程直接退出。这是最常被忽略的起点。
根本原因是 asyncio.run() 内部使用的事件循环未注册信号处理器,且其异常传播机制不包含对系统信号的响应。
- 必须手动获取并配置事件循环(
asyncio.get_event_loop()或asyncio.new_event_loop()) - 使用
loop.add_signal_handler()注册SIGTERM和SIGINT(别漏掉SIGINT,Ctrl+C 也得优雅) - 注册的回调函数不能是 async 函数,必须是同步函数(否则会报
RuntimeError: Cannot add async function as signal handler)
如何安全取消正在运行的 asyncio 任务?用 asyncio.create_task() + cancel() + gather()
收到信号后,不能直接 sys.exit(),而要主动取消所有活跃任务,并等待它们完成清理逻辑(比如关闭连接、提交日志、保存状态)。关键在于:取消 ≠ 立即终止;它只是设置 CancelledError 异常,协程需自行响应。
- 用
asyncio.all_tasks(loop)获取当前所有未完成任务,排除自身信号处理任务(避免死锁) - 对每个任务调用
task.cancel(),但注意:未 await 的任务不会立即响应取消,必须显式await asyncio.gather(*tasks, return_exceptions=True) -
return_exceptions=True很重要——否则任一任务抛出CancelledError都会让gather()提前退出,其他任务可能被跳过 - 在协程内部,用
try/except CancelledError:或finally:块做资源释放,不要依赖__del__或后台线程
为什么 await asyncio.sleep() 不会被 cancel() 中断?检查你的 awaitable 是否支持取消
某些低层 I/O 操作或第三方库的 awaitable(如某些数据库驱动、HTTP 客户端封装)可能未正确传播取消信号,导致 task.cancel() 调用后,协程卡在 await some_slow_call() 上不动,整个 shutdown 卡住。
- 优先选用原生支持取消的标准库 awaitable(
asyncio.sleep()、asyncio.wait_for()、asyncio.open_connection()等都 OK) - 对第三方库,查文档确认是否声明 “cancellation-aware”;例如
aiohttp.ClientSession.get()支持取消,但裸写的socket.recv()包装就不行 - 加超时兜底:
await asyncio.wait_for(task, timeout=10.0),避免单个任务拖垮整体退出流程 - 测试时可故意发
kill -TERM $(pidof python),观察进程是否在几秒内退出,而不是 hang 住
生产环境必须处理的边界情况:子进程、线程、同步阻塞调用
asyncio 只能管理协程和基于事件循环的异步 I/O。一旦你启动了 subprocess.Popen、threading.Thread 或调用了 time.sleep() 这类同步阻塞操作,SIGTERM 处理就不再“自动”覆盖它们。
- 子进程:用
asyncio.create_subprocess_exec()替代subprocess.Popen,它返回的Process对象有terminate()和wait()方法,可在 shutdown 回调中调用 - 线程:若必须用线程(如调用 C 库),设
daemon=True,并在主线程 shutdown 时显式调用thread.join(timeout=5) - 同步阻塞调用:改用
loop.run_in_executor()包装,并确保 executor 支持 shutdown(concurrent.futures.ThreadPoolExecutor.shutdown(wait=True)) - 全局状态(如文件句柄、临时目录)应在
finally块中清理,而非依赖 atexit —— atexit 回调在 asyncio 事件循环已关闭后执行,可能出错
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











