
事情发生在一个周一的早上。
数据同事跑过来找我,说训练脚本在 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() 只能调用一次。**
又过了两周,代码上线了。我在一个已有的异步服务里,调用了一个第三方库的函数。
那个函数里面是这样写的:
# 第三方库的代码
def do_something_sync():
loop = asyncio.get_event_loop()
loop.run_until_complete(some_async_task())我的服务本来就在事件循环里跑着,调用了这个函数之后——
RuntimeError: This event loop is already runningrun_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()是让你在已有循环上执行任务的"底层工具"。前者省事但限制多,后者灵活但责任大。
选哪个?看你的场景。但不管选哪个,记住一条铁律:
别在已经运行的事件循环里再启动另一个事件循环。
这条规则救了我三次。希望也能救你一次。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。