绝大多数 RuntimeError: Event loop is closed 报错,是因为在事件循环已关闭后仍尝试操作它,常见于 asyncio.run() 后再调 close()、async for 未耗尽、跨线程误用 get_event_loop() 或 pytest fixture 清理顺序错误。

直接说结论:绝大多数 RuntimeError: Event loop is closed 报错,不是因为你代码写错了逻辑,而是你**在事件循环已经退出后,还试图操作它**——常见于手动关循环、异步生成器没耗尽、跨线程误用 get_event_loop(),或测试中 fixture 清理顺序错乱。
asyncio.run() 后再调 loop.close() 就必然报错
Python 3.7+ 的 asyncio.run() 是“全包服务”:自动创建新循环 → 运行主协程 → 关闭循环。你再显式调一次 loop.close(),等于对一个已失效对象做无效操作。
- 典型错误模式:
asyncio.run(main())之后,又写了asyncio.get_event_loop().close()或在atexit/__del__里偷偷调了close() - 调试线索:报错堆栈里出现
_ProactorBasePipeTransport.__del__,基本就是这个原因 - 正确做法:只用
asyncio.run(main())启动;如需多次运行(比如 REPL 或测试),改用asyncio.new_event_loop()+run_until_complete()+close(),且确保每个循环只 close 一次
async for 没跑完就丢掉异步生成器
异步生成器(async def + yield)如果没被完整消费,GC 时会尝试调用 agen.aclose() 清理,但此时循环很可能已被 asyncio.run() 关闭,于是触发 RuntimeError,并常连带抛出 async_generator_athrow(): object is not an async generator。
- 常见场景:装饰器返回异步生成器但没保证上层遍历完;缓存了
agen却没在退出前显式关闭 - 安全写法:用
try/finally包裹async for,确保await agen.aclose()被调用(注意是aclose(),不是close()) - 快速验证:在生成器函数末尾加
print("agen done"),确认是否真执行到了 yield 结束之后
跨线程调用 create_task() 或 run_in_executor()
事件循环绑定到创建它的线程。子线程里直接调 asyncio.get_event_loop().create_task(),要么拿不到 loop(报 There is no current event loop in thread 'xxx'),要么拿到的是主线程那个已被关闭的 loop(进而触发 Event loop is closed)。
立即学习“Python免费学习笔记(深入)”;
- 正确跨线程提交任务:必须用
loop.call_soon_threadsafe()包裹create_task(),不能裸调 - 子线程中不要依赖
get_event_loop();应由主线程把 loop 实例传入,或改用asyncio.run_coroutine_threadsafe(coro, loop) - 尤其注意日志里有没有
There is no current event loop—— 这往往是后续Event loop is closed的前置信号
pytest-asyncio 中 fixture 清理顺序错乱
当 fixture 声明为 scope="function" 且是 async def,pytest-asyncio 默认会在每个 test 后 await cleanup。但如果 cleanup 协程里用了全局 loop(比如没传参、直接 get_event_loop()),而此时主循环已被 asyncio.run() 关闭,就会爆错。
- 典型表现:单测通过,多个 test 连续跑就崩;或
event_loopfixture 里用了get_running_loop()但没 fallback 到new_event_loop() - 稳妥写法:
@pytest.fixture中显式创建并管理 loop,例如:
import asyncio
import pytest
@pytest.fixture
def event_loop():
loop = asyncio.new_event_loop()
yield loop
if not loop.is_closed():
loop.close()
复杂点在于:loop 的生命周期必须和 fixture 严格对齐,且不能假设主线程 loop 可用——这点最容易被忽略,尤其在混合使用 TestClient 和自定义异步 fixture 的 FastAPI 项目中。


















