asyncio.run()无法优雅处理SIGINT/SIGTERM,因内部接管信号且不监听;正确做法是手动创建事件循环、提前注册信号处理器、遍历取消任务并显式清理异步资源。

asyncio.run() 里无法直接捕获 SIGINT/SIGTERM
直接在 asyncio.run() 外层用 signal.signal() 注册处理器基本无效——因为 asyncio.run() 内部会接管信号,且事件循环启动后才真正监听。你看到的 Ctrl+C 中断只是触发了默认的 KeyboardInterrupt,但此时事件循环可能正在执行协程,资源未释放、任务未 cancel,导致连接泄漏或文件句柄残留。
正确做法是手动管理事件循环,并在启动前注册信号处理器:
import asyncio import signal <p>async def main():</p><h1>你的主协程逻辑</h1><pre class="brush:php;toolbar:false;">await asyncio.sleep(10)
def handle_exit(sig, frame): print(f"Received {sig}, shutting down...") loop = asyncio.get_running_loop() for task in asyncio.all_tasks(loop): if not task.done(): task.cancel()
loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.add_signal_handler(signal.SIGINT, handle_exit, signal.SIGINT, None) loop.add_signal_handler(signal.SIGTERM, handle_exit, signal.SIGTERM, None)
try: loop.run_until_complete(main()) finally: loop.close()
使用 asyncio.create_task() 后必须显式 await 或 cancel
很多人用 asyncio.create_task() 启动后台任务(比如心跳、日志刷新),却忘了它们不会自动被事件循环“回收”。如果主协程结束而这些 task 还在运行,loop.run_until_complete() 会立即返回,task 被丢弃但仍在后台执行——这属于未定义行为,可能引发竞态或资源泄露。
安全退出的关键是:在收到信号后,遍历所有未完成 task 并调用 cancel(),然后 await asyncio.gather(*tasks, return_exceptions=True) 等待它们真正结束:
- 不要依赖
loop.shutdown_asyncgens()或loop.close()自动清理 task - 务必在
handle_exit中调用loop.stop(),否则run_until_complete()不会退出 - 如果 task 内部有
try/except CancelledError,需确保它能及时释放资源(如关闭 aiohttp.ClientSession)
在 aiohttp / aiomysql 等库中,client 对象必须显式 close()
像 aiohttp.ClientSession、aiomysql.Pool、aioredis.Redis 这类对象,其 __aexit__ 或 close() 方法是异步的,不能在 signal handler 这种同步上下文中直接 await。常见错误写法:session.close()(这是个协程对象,不 await 就等于没调用)。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
解决方式是在退出流程中把 cleanup 作为协程加入事件循环:
session = None <p>async def main(): global session session = aiohttp.ClientSession() await asyncio.sleep(10)</p><p>async def cleanup(): if session and not session.closed: await session.close()</p><p>def handle_exit(sig, frame): loop = asyncio.get_running_loop() loop.create_task(cleanup()) # ✅ 推入事件循环 for task in asyncio.all_tasks(loop): if not task.done(): task.cancel() loop.stop() </p>
Windows 下 SIGTERM 不可用,需兼容不同平台
Windows 不支持 SIGTERM,调用 loop.add_signal_handler(signal.SIGTERM, ...) 会直接抛出 ValueError: signal only works in main thread(即使你在主线程)。这不是 bug,是 Windows 的限制。
跨平台健壮写法是:
- 始终检查
hasattr(signal, "SIGTERM") - 对 Windows,可监听
signal.CTRL_C_EVENT或signal.CTRL_BREAK_EVENT(仅限控制台进程) - 更通用的做法:用
asyncio.Event配合后台线程监听keyboard或系统信号(如通过win32event),但复杂度上升;多数服务场景建议只保SIGINT+ 日志提示
真正难的不是注册信号,而是让每个异步资源都提供可 await 的 cleanup 接口,并在 cancel 后仍能可靠释放——这点常被忽略,直到压测时连接数暴涨才暴露。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










