在 Python 异步程序里,RuntimeError: Event loop is closed 很少只是“这一行代码写错了”。更常见的情况是:事件循环已经被 asyncio.run() 关闭,但后续代码、子线程,或某个异步库对象还在继续引用它。要把问题真正修好,关键不是临时绕过异常,而是把 loop 的创建、使用和释放边界理顺。
下面按几个最常见的触发场景展开说明:先看为什么 asyncio.run() 后最容易报错,再看多线程和第三方库里的隐藏坑,最后给出定位问题时最实用的诊断办法,帮助你判断到底是哪一段代码还在操作已经关闭的 loop。
为什么 asyncio.run() 之后最容易报这个错
asyncio.run() 的行为非常明确:它会创建一个新的事件循环、运行目标协程、结束后再显式关闭这个 loop。也就是说,一旦 asyncio.run() 返回,这个 loop 的生命周期通常就已经结束了。

因此,下面这种写法就是典型触发场景:
asyncio.run(main()) asyncio.get_event_loop().create_task(...)
第一行执行完成后,相关 loop 已经关闭;第二行再去获取并操作 loop,就可能直接抛出 RuntimeError: Event loop is closed。
这一类问题该怎么改
- 不要在
asyncio.run()调用结束后继续使用asyncio.get_event_loop()。 - 如果需要多次运行异步逻辑,可以改用
asyncio.new_event_loop()+loop.run_until_complete()手动管理事件循环,但要同时处理好线程边界。 - 不要把已经关闭的 loop 缓存在全局变量、单例对象或模块状态里反复复用。
一句话总结:asyncio.run() 适合把一次完整的异步执行包起来,但不适合在它结束后继续拿内部 loop 做额外操作。
多线程场景里,最容易踩的是跨线程误用 loop
asyncio 的事件循环默认绑定到创建它的线程。很多“明明代码没问题却突然报错”的情况,实际上不是 loop 本身有问题,而是 loop 被错误地跨线程传递和访问了。
例如,主线程里调用过 asyncio.run() 并正常退出后,loop 已经关闭;这时如果子线程又通过闭包、全局变量或某些未隔离的模块状态引用到了主线程的 loop,就很容易触发同样的异常。
多线程下的处理原则
- 子线程里不要依赖
asyncio.get_event_loop()“顺手拿一个 loop”来用。 - 更稳妥的写法是显式创建并绑定当前线程的事件循环:
asyncio.set_event_loop(asyncio.new_event_loop())。 - 如果线程中要运行异步逻辑,建议每个线程都使用自己的
asyncio.new_event_loop()+loop.run_until_complete()。 - 排查时别只盯业务代码,也要检查
logging、atexit、装饰器、ORM 或 SDK 初始化逻辑里是否偷偷调用了get_event_loop()。
这里的核心判断标准很简单:loop 属于哪个线程,就只应由那个线程创建、运行和关闭。

用 aiohttp、aiomysql 时,问题常出在资源生命周期
不少异步库会在内部持有当前 loop 的引用,例如 aiohttp.ClientSession、aiomysql.create_pool。如果这些对象在 loop 已经关闭之后才析构,报错就会变得很常见。

最典型的情况是:对象在 asyncio.run() 的主协程外部创建,或者被保存为模块级全局变量,等到程序退出或垃圾回收时才尝试清理资源。此时它关联的 loop 可能早就关掉了。
更稳妥的写法
- 优先使用
async with显式管理生命周期。
async with aiohttp.ClientSession() as session:
...
- 不要把
session或连接池保存为模块级全局变量;它们并不是线程安全或 loop 安全的。 - 如果确实需要复用资源,确保它和 loop 处在同一个生命周期内,例如在
asyncio.run()的主协程里创建,再传给子协程使用,而不是在外部提前初始化。
很多看起来像“库本身不稳定”的报错,实际都是资源对象存活时间超过了所属 loop 的存活时间。
排查时先打印 loop 状态,比直接猜原因更有效
只看异常文本,通常很难判断到底是“调用前 loop 已经关闭”,还是“某一步调用后立刻被关闭”。这时最直接的办法,就是在关键位置打印 loop 状态:
loop = asyncio.get_event_loop()
print(f"Loop status: running={loop.is_running()}, closed={loop.is_closed()}")
这段输出能帮助你快速确认两个事实:
- 当前 loop 是否正在运行;
- 当前 loop 是否已经关闭。
尤其要注意,asyncio.run() 返回后,loop.is_closed() 几乎总是 True。这不是异常现象,而是它的正常行为。
调试时还要注意这几点
- 不要把
loop.is_closed()当成恢复逻辑的入口,因为已关闭的 loop 不能重启。 - 在单元测试里如果频繁启停异步代码,优先使用
pytest-asyncio统一管理 loop,而不是到处手写asyncio.run()。 - 像
fastapi、httpx这样的第三方库,可能封装了自己的 loop 管理逻辑,遇到报错时先查文档,确认官方推荐的调用方式。
真正要修的,是 loop 的生命周期边界
RuntimeError: Event loop is closed 的直接原因通常很明确:代码还在操作一个已经关闭的事件循环。但更值得重视的是,这类异常常常暴露出更深层的问题,比如多个框架同时管理 loop、全局状态里残留旧 loop,或者异步资源没有跟着主协程及时释放。
处理这类问题时,可以按这个顺序检查:先确认 loop 是在哪里创建的,再看它由谁关闭,最后核对 session、连接池、任务对象和线程代码是否仍在引用旧 loop。把这三条边界理顺,通常比单纯捕获这个 RuntimeError 更有效。







