asyncio.run()后手动close()、async for未耗尽异步生成器、子线程直接get_event_loop()、pytest-asyncio fixture清理顺序错乱,是RuntimeError: Event loop is closed的四大主因。

别急着 close(),也别在子线程里直接 get_event_loop() —— 90% 的 RuntimeError: Event loop is closed 都源于这两类误用。
asyncio.run() 后还调 loop.close()?立刻停手
Python 3.7+ 中 asyncio.run() 是“全包式”启动器:它自动创建新 loop、运行协程、关闭 loop 并清理资源。你再手动调一次 loop.close(),就是在对一个已销毁的对象发指令。
- 常见错误写法:
asyncio.run(main())后紧跟asyncio.get_event_loop().close() - 调试线索:报错堆栈末尾常带
loop.close()或出现在atexit/__del__里 - 正确做法:删掉所有显式的
close();如需复用 loop(比如 pytest 多次测试),改用asyncio.new_event_loop()+run_until_complete()+ 显式close(),且确保只关一次
async for 没耗尽异步生成器?会悄悄炸
未完整消费的异步生成器(async def + yield)在 GC 时尝试自我清理,但此时 loop 往往已被 asyncio.run() 关闭,于是抛出 RuntimeError,并可能附带 async_generator_athrow(): object is not an async generator 这类迷惑性次级错误。
- 典型场景:装饰器返回异步生成器、缓存层提前 return、或
async for被break中断后没调agen.aclose() - 安全写法:用
try/finally包裹async for,确保agen.aclose()执行(注意不是.close()) - 验证技巧:在生成器函数末尾加
print("done"),确认是否真走到了 yield 结束之后
子线程里调 asyncio.get_event_loop()?必然失败
事件循环是线程局部的。主线程有默认 loop,但子线程没有 —— 直接调 asyncio.get_event_loop() 就会先报 There is no current event loop in thread 'xxx',后续再操作就容易连锁触发 Event loop is closed。
立即学习“Python免费学习笔记(深入)”;
- 错误模式:在
ThreadPoolExecutor的 worker 函数里直接 await 或调loop.create_task() - 可靠方案:用
asyncio.run_coroutine_threadsafe(coro, loop),把协程提交给主线程的 loop 执行 - 替代思路:子线程中自己新建 loop(
new_loop = asyncio.new_event_loop()),再调asyncio.set_event_loop(new_loop),但必须确保该线程生命周期内不重复 set 或 close
pytest-asyncio fixture 清理顺序错乱?查 scope 和 await 时机
当 fixture 声明为 scope="function" 且含 async def,pytest-asyncio 默认在每个 test 后 await 其 cleanup 协程。如果 cleanup 里用了已关闭的 loop(比如主线程 loop 已随上个 asyncio.run() 结束),就会爆 Event loop is closed。
- 高频诱因:多个 test 共享同一个 loop 实例,或 cleanup 协程里调了
asyncio.get_event_loop() - 推荐写法:cleanup 协程不依赖全局 loop,改用传入的 loop 参数;或把 fixture 改为
scope="session"并显式管理 loop 生命周期 - 调试提示:在 fixture setup/cleanup 中打印
id(asyncio.get_event_loop()),确认是否复用或已失效
最易被忽略的一点:错误往往不是孤立发生的 —— There is no current event loop 是前兆,Event loop is closed 是后果,而真正的问题可能藏在异步生成器没关、atexit 里多关了一次、或某个第三方库偷偷调了 loop.close()。盯住第一次异常出现的位置,比盲目补 try/except 有用得多。



















