第一个通宵:Jupyter Notebook 里跑崩了
事情发生在一个周一的早上。
数据同事跑过来找我,说训练脚本在 Jupyter Notebook 里跑不起来,报了个错:
RuntimeError: asyncio.run() cannot be called from a running event loop
我看了眼代码,没问题啊——标准的 asyncio 写法:
async def fetch_data():
# 从数据库异步拉取训练数据
return await db.query("SELECT * FROM training_set")
def load_training_data():
data = asyncio.run(fetch_data())
return data
在命令行跑得好好的,一放到 Jupyter 里就报错。
我查了半天,最后发现原因特别简单:Jupyter Notebook 自己已经跑着一个事件循环了。
asyncio.run() 的底层逻辑是:创建一个全新的事件循环,用它执行协程,执行完了关闭它。
但 Jupyter 里已经有一个正在运行的事件循环了——asyncio.run() 检测到"同一个线程里已经有事件循环在跑",直接拒绝工作。
这就好比:你进了一家餐厅,服务员已经在了,你非要再雇一个服务员——餐厅说不行,一个线程只能有一个事件循环。
解决方案?在 Jupyter 里直接用 await,别用 asyncio.run():
# 在 Jupyter 里直接这样写
data = await fetch_data()
第一个通宵就这么过去了。我心想:行吧,asyncio.run() 不能在已有循环的环境里调用,记住了。
但我没想到,这只是个开始。
第二个通宵:事件循环已关闭
两周后,我写了一个数据处理流水线,需要依次执行三个异步任务:
async def step1(): ...
async def step2(): ...
async def step3(): ...
# 我的写法
asyncio.run(step1())
asyncio.run(step2())
asyncio.run(step3())
跑完 step1,到 step2 的时候报错了:
RuntimeError: Event loop is closed
我懵了。每个 step 单独跑都没问题,串起来就崩?
查了源码才明白:**asyncio.run() 每次调用,都会走一遍"创建→运行→关闭"的完整生命周期**。
第一次 asyncio.run(step1()) 跑完,事件循环就被关闭了。第二次调用的时候,它试图创建一个新的循环,但之前的关闭动作留下了残留状态。
asyncio.run() 这个函数的设计初衷,是让你在程序的最高层调用一次——作为整个异步程序的入口点。它不是给你反复调用的。
正确的写法应该是:
async def main():
await step1()
await step2()
await step3()
asyncio.run(main()) # 只调用一次
或者,如果你确实需要在同一个事件循环里执行多个独立操作,用 loop.run_until_complete():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(step1())
loop.run_until_complete(step2())
loop.run_until_complete(step3())
finally:
loop.close()
run_until_complete() 不会关闭循环,可以多次调用。但注意,你手动管理循环的创建和关闭——忘了 close() 就会资源泄漏。
第二个通宵,我又学到了:**asyncio.run() 只能调用一次。**
第三个通宵:在已有的循环里调用 run_until_complete
又过了两周,代码上线了。我在一个已有的异步服务里,调用了一个第三方库的函数。
那个函数里面是这样写的:
# 第三方库的代码
def do_something_sync():
loop = asyncio.get_event_loop()
loop.run_until_complete(some_async_task())
我的服务本来就在事件循环里跑着,调用了这个函数之后——
RuntimeError: This event loop is already running
run_until_complete() 的规则是:它只能在没有运行中的循环时被调用。如果循环已经在跑了,你再调 run_until_complete() 就会冲突。
这就好比:车已经发动了,你非要再拧一次钥匙。
那怎么办?如果循环已经在运行,你应该用 create_task() 或者 await,而不是再调 run_until_complete()。
# 在已经运行的事件循环里
task = asyncio.create_task(some_async_task())
# 或者直接 await
await some_async_task()
第三个通宵教会了我:**run_until_complete() 也不能在有运行循环的时候随便调。**
所以到底有什么区别?
折腾了三个通宵,我终于搞清楚了这两个东西的本质区别。
asyncio.run() —— 全包服务员
- 创建一个全新的事件循环
- 运行你传入的协程
- 关闭循环,清理所有资源(异步生成器、线程池等)
- 缺点:每次调用都创建新循环,不能在有循环的环境里调用,不能多次调用
- 适用场景:脚本的主入口,调用且仅调用一次
loop.run_until_complete() —— 半自动服务员
- 在已有的事件循环上运行协程
- 运行完不会关闭循环
- 缺点:你得自己管理循环的创建和关闭;循环已经在跑的时候不能调用
- 适用场景:需要复用同一个循环执行多个任务,或者你手动控制事件循环的生命周期
一张图看清楚:
| asyncio.run() | loop.run_until_complete() | |
| 创建事件循环 | ✅ 自动创建 | ❌ 需要手动 |
| 关闭事件循环 | ✅ 自动关闭 | ❌ 需要手动 |
| 能调用几次 | 1次 | 多次(同一循环) |
| 能在已有循环里调 | ❌ 报错 | ❌ 报错 |
| 适合场景 | 脚本主入口 | 手动管理循环 / 复用循环 |
源码里藏着答案
看一眼 asyncio.run() 的简化源码就全明白了:
def run(main, *, debug=False):
# 检查是否已有运行中的循环——有就报错
if events._get_running_loop() is not None:
raise RuntimeError("asyncio.run() cannot be called from a running event loop")
# 创建全新的循环
loop = events.new_event_loop()
try:
events.set_event_loop(loop)
# 本质上调用的就是 run_until_complete
return loop.run_until_complete(main)
finally:
# 清理:取消所有任务、关闭异步生成器、关闭循环
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
看到了吗?**asyncio.run() 底层就是在调用 loop.run_until_complete()**,只不过在外面包了一层"创建+清理"的逻辑。
asyncio.run() 存在的意义就是:帮你把"创建循环、运行、关闭循环"这三件事打包在一起,让你少写几行样板代码。
但打包的代价是——你失去了对循环生命周期的控制权。你想复用循环?不行。你想在已有循环里调用?不行。
实战:什么时候用什么?
场景一:写一个独立的脚本
用 asyncio.run(),调用一次,简洁干净:
async def main():
# 你的异步逻辑
pass
if __name__ == "__main__":
asyncio.run(main())
场景二:在 Jupyter / IPython 里写代码
别用 asyncio.run(),直接用 await:
# Jupyter 里直接 await
result = await my_async_function()
场景三:需要依次执行多个独立任务,复用同一个循环
用手动管理的方式:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(task1())
loop.run_until_complete(task2())
loop.run_until_complete(task3())
finally:
loop.close()
或者用 Python 3.11+ 的 asyncio.Runner:
with asyncio.Runner() as runner:
runner.run(task1())
runner.run(task2())
runner.run(task3())
场景四:在已有的异步函数里调用另一个异步函数
直接用 await,别搞花活:
async def main():
result = await another_async_function()
三个通宵换来的教训
第一个通宵:asyncio.run() 不能在已有循环的环境里调用。
第二个通宵:asyncio.run() 只能调用一次。
第三个通宵:run_until_complete() 也不能在有运行循环的时候调用。
这三个教训其实指向同一个核心原则:
事件循环是单线程里的唯一调度器,一个线程同一时间只能有一个事件循环在运行。
asyncio.run() 帮你管理这个循环——创建它、运行它、关闭它。你要么完全交给它管(只调用一次),要么自己管(用 loop.run_until_complete() 并手动管理生命周期)。别混着用。
现在回想起来,这三个通宵其实可以归结为一句话:
asyncio.run()是为你创建并管理事件循环的"一站式服务";loop.run_until_complete()是让你在已有循环上执行任务的"底层工具"。前者省事但限制多,后者灵活但责任大。
选哪个?看你的场景。但不管选哪个,记住一条铁律:
别在已经运行的事件循环里再启动另一个事件循环。
这条规则救了我三次。希望也能救你一次。