asyncio.run() 会静默处理 sigint 导致 keyboardinterrupt 无法被捕获,正确做法是手动创建事件循环并用 add_signal_handler 注册同步中断处理函数,配合 asyncio.event 和 cancellederror 处理协程取消。

asyncio.run() 会吞掉 KeyboardInterrupt
直接用 asyncio.run(main()) 启动协程时,一旦按下 Ctrl+C,你很可能看不到 KeyboardInterrupt 被 except 捕获——不是没发生,而是事件循环在抛出前就终止了,你的 except KeyboardInterrupt 根本没机会执行。
根本原因是 asyncio.run() 内部做了信号拦截和静默处理,它把 SIGINT 转成任务取消,并调用 loop.close(),绕过了 Python 默认的异常传播路径。
- 验证方法:在
main()开头加print("start"),再按 Ctrl+C;你会发现 print 执行了,但 except 块不触发 - 替代方案:手动创建并运行 loop,保留对信号的控制权
- 最简修复:
asyncio.run(main(), debug=False)不起作用,必须换写法
add_signal_handler 是唯一可靠入口
在 asyncio 中,KeyboardInterrupt 本质是 SIGINT 信号,而 asyncio 的正确响应方式不是靠 try-except,而是用 loop.add_signal_handler(signal.SIGINT, handler) 注册回调。
这个 handler 必须是同步函数(不能是 async),且需主动调用 loop.stop() 或 loop.create_task() 来启动清理逻辑。
仅使用 Python 将 Claw Code 虚拟化框架与 OpenClaw 集成,提供 184 种工具和 200 多条命令,用于安全分析和代码质量...
- 别在 handler 里 await 任何东西——它运行在信号上下文中,不进事件循环
- 清理任务要包装成
loop.create_task(cleanup()),否则不会执行 - 记得在 handler 里设个标志(如
shutdown_event.set()),让主协程感知退出意图
多任务并发时 cleanup 容易漏掉正在 await 的协程
即使你注册了 SIGINT handler 并调用了 loop.stop(),那些正在 await asyncio.sleep()、await aiohttp.get() 或 await queue.get() 的协程,可能根本收不到取消信号,直接卡住。
asyncio 的任务取消依赖协程主动检查 task.cancelled() 或在可中断点(如 await)抛出 CancelledError。如果协程卡在阻塞 IO 或没做取消检查,就会跳过清理步骤。
- 所有长时间 await 的地方,都该包一层
try/except CancelledError - 避免裸用
time.sleep(),换成await asyncio.sleep()才能被取消 - HTTP 客户端要用支持 timeout 和 cancel 的库(如
aiohttp自带 cancel 支持,httpx.AsyncClient也行)
threading.Event 在 asyncio 里不管用
有人想复用多线程那套“主线程设 flag,子线程轮询”的思路,在 asyncio 里用 threading.Event 控制协程退出——这不行。因为 threading.Event.wait() 是阻塞调用,会卡死整个事件循环。
asyncio 场景下必须用 asyncio.Event,它的 wait() 是 awaitable,能交还控制权。
-
threading.Event→ 只能在同步代码或线程中用 -
asyncio.Event→ 才能在协程中安全 await - 混合使用(比如在 handler 里 set
asyncio.Event)要确保 loop 正在运行,否则set()会失效
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










