asyncio.run() 中 Ctrl+C 直接崩溃是因为其默认将 SIGINT 视为致命信号并调用 sys.exit(1),跳过所有清理逻辑;需手动管理事件循环、拦截信号、取消任务并 await 清理。

asyncio.run() 里按 Ctrl+C 为什么直接崩溃而不是优雅退出
因为 asyncio.run() 默认把 SIGINT 当作致命信号处理,一收到就立刻调用 sys.exit(1),跳过所有 finally、__aexit__ 和取消任务的清理逻辑。这不是 bug,是设计如此 —— 它假设你只写简单脚本,不关心资源释放。
真正需要优雅中断的场景(比如正在上传文件、写数据库、维持 WebSocket 连接),必须绕过 asyncio.run() 自己管理事件循环。
- 用
asyncio.new_event_loop()创建 loop - 手动调用
loop.run_forever() - 通过
loop.add_signal_handler()拦截SIGINT - 在信号处理器中调用
loop.stop(),再等所有 task 清理完毕
如何让 asyncio 任务响应 SIGINT 并执行 cancel + await cleanup
关键不是“捕获信号”,而是让信号触发一个协程去取消所有运行中任务,并等待它们完成清理。不能在信号处理器里直接 await(它运行在同步上下文),必须用 loop.create_task() 把清理逻辑交还给事件循环。
典型结构:
def signal_handler():
# 同步函数:取消所有 pending task
for task in asyncio.all_tasks(loop):
if not task.done():
task.cancel()
# 异步清理:启动 shutdown 协程
loop.create_task(shutdown())
<p>async def shutdown():</p><h1>这里可以 await 关闭连接、flush 缓存、保存状态</h1><pre class="brush:php;toolbar:false;">await close_db_connection()
await close_websocket()
# 最后让 loop 退出
loop.stop()
注意:task.cancel() 只是设 flag,真正退出要靠协程内部检查 if cancelled(): return 或用 try/except CancelledError: 包裹。
loop.run_forever() + signal.pause() 的兼容性陷阱
在 Linux/macOS 上,loop.add_signal_handler() 工作良好;但在 Windows 上会报错 ValueError: signal only works in main thread —— 因为 Windows 不支持从非主线程调用 signal.signal()。
跨平台安全做法是:
- Windows 下改用
loop.set_exception_handler()配合asyncio.ensure_future()轮询keyboardinterrupt(不推荐) - 更可靠的是统一用
loop.add_signal_handler(signal.SIGINT, ...),但只在非 Windows 环境启用;Windows 下改监听Ctrl+C通过threading.Event通知主循环 - 或者直接放弃原生信号,改用
asyncio.to_thread()在子线程里阻塞等待signal.pause(),再用loop.call_soon_threadsafe()触发 shutdown
多数生产服务跑在 Linux,所以优先保证 POSIX 行为正确,Windows 仅做基本 fallback 即可。
asyncio.CancelledError 被吞掉导致 cleanup 不执行
这是最隐蔽的问题:你在 task 里写了 try/except Exception:,结果 CancelledError 被这个宽泛的 except 捕获并静默吞掉,cleanup 逻辑永远不运行。
必须显式处理:
try:
await long_running_job()
except asyncio.CancelledError:
await cleanup_resources() # 必须在这里做
raise # 再抛出,让外层知道已取消
except Exception as e:
log.error(e)
raise
另外,如果 cleanup 本身也 await,它可能再次被 cancel —— 所以建议 cleanup 内部用 asyncio.shield() 包一层,防止二次中断。
真正难的不是写 shutdown 逻辑,而是确保每个 await 点都对 CancelledError 敏感,且不屏蔽它。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











