如何调试Python asyncio抛出的Event loop is closed异常?
问题产生原因
这个报错是Windows平台下Python 3.8~3.11版本使用默认ProactorEventLoop时的典型问题,核心触发逻辑:
- 事件循环执行
close()操作后,仍有未被显式释放的_ProactorBasePipeTransport实例(对应异步子进程管道、套接字、异步IO流等底层传输对象)存在于内存中。 - 当Python GC回收这些残留传输对象时,会自动调用类的
__del__析构方法尝试执行资源关闭,该方法会尝试向已经关闭的事件循环投递关闭回调,触发Event loop is closed异常。 - 注意报错开头的
Exception ignored in说明这个异常是析构阶段抛出的,会被解释器直接忽略,不会导致程序崩溃,只是会打印错误栈。
常见的触发场景包括:
- 程序退出/手动关闭事件循环前,没有await所有正在运行的异步任务,部分IO操作被强制中断,底层传输对象没有走正常的关闭流程
- 多线程场景下跨线程传递事件循环对象,或者非主线程创建的事件循环没有在所属线程内完成完整的资源清理就直接退出线程
- 部分第三方异步库没有正确处理任务取消逻辑,循环关闭时残留了未释放的传输句柄
- Python本身的已知缺陷:Windows平台Proactor事件循环的默认清理顺序存在问题,即使正确关闭所有任务,也可能因为GC回收顺序问题触发这个误报。
可行调试方法
- 开启asyncio原生调试模式
在程序最开头添加如下代码开启调试:
也可以在启动程序前设置环境变量import asyncio asyncio.get_event_loop().set_debug(True)PYTHONASYNCIODEBUG=1。开启后asyncio会自动记录所有未被await的协程、未正常关闭的传输对象的创建调用栈,直接输出到日志,大部分场景下可以直接定位到残留操作的代码位置。 - 给传输对象加创建栈追踪
如果默认调试日志信息不足,可以通过猴子补丁给所有_ProactorBasePipeTransport实例绑定创建时的调用栈,出现问题时直接读取栈信息定位:
排查时只要打印残留传输对象的import traceback from asyncio.proactor_events import _ProactorBasePipeTransport original_init = _ProactorBasePipeTransport.__init__ def patched_transport_init(self, *args, **kwargs): original_init(self, *args, **kwargs) self._create_traceback = "".join(traceback.format_stack()) _ProactorBasePipeTransport.__init__ = patched_transport_init_create_traceback属性,就能拿到该对象是被哪段代码创建的。 - 逐线程排查事件循环
多线程场景下给每个线程分配独立的事件循环,禁止跨线程传递loop引用。每个线程退出前严格执行清理流程:先取消所有运行中任务,await任务取消完成,再依次执行loop.shutdown_asyncgens()、loop.shutdown_default_executor(),最后调用loop.close()。逐个线程验证关闭后是否还会触发报错,缩小问题范围。 - 给事件循环绑定自定义异常处理器
给所有创建的事件循环设置set_exception_handler回调,在回调中记录异常发生时的事件循环状态、所属线程、待执行任务列表,定位异常触发时的上下文。
常规规避方案
如果排查后确认不存在实际资源泄漏,只是Python本身的清理顺序问题导致的误报,可以选择以下方案屏蔽报错:
- 切换事件循环实现为SelectorEventLoop,在程序入口添加如下代码:
注意该方案下asyncio的子进程相关API无法使用,如果程序依赖异步子进程功能不要选择这个方案。import sys import asyncio if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - 补丁修正析构逻辑,在程序入口添加如下代码,忽略析构阶段循环关闭的异常:
from asyncio.proactor_events import _ProactorBasePipeTransport def safe_transport_del(self): try: self.close() except RuntimeError: pass _ProactorBasePipeTransport.__del__ = safe_transport_del
内容的提问来源于stack exchange,提问作者Ryan Glenn
相关产品推荐
相关产品推荐

