以为 asyncio.run() 就是简单的启动?它和 loop.run_until_complete() 的区别让我项目崩了3次

简介: 本文以三次通宵调试为线索,深入剖析 `asyncio.run()` 与 `loop.run_until_complete()` 的本质区别:前者是“一站式服务”,自动创建并关闭事件循环,仅限主入口调用一次;后者是“底层工具”,需手动管理循环生命周期,适用于复用场景。核心铁律:**一个线程同一时间只能有一个运行中的事件循环**。(239字)

第一个通宵: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 自己已经跑着一个事件循环了

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (55).png

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() 是让你在已有循环上执行任务的"底层工具"。前者省事但限制多,后者灵活但责任大。

选哪个?看你的场景。但不管选哪个,记住一条铁律:

别在已经运行的事件循环里再启动另一个事件循环。

这条规则救了我三次。希望也能救你一次。

目录
相关文章
|
存储 缓存 文件存储
如何保证分布式文件系统的数据一致性
分布式文件系统需要向上层应用提供透明的客户端缓存,从而缓解网络延时现象,更好地支持客户端性能水平扩展,同时也降低对文件服务器的访问压力。当考虑客户端缓存的时候,由于在客户端上引入了多个本地数据副本(Replica),就相应地需要提供客户端对数据访问的全局数据一致性。
33079 82
如何保证分布式文件系统的数据一致性
|
前端开发 容器
HTML5+CSS3前端入门教程---从0开始通过一个商城实例手把手教你学习PC端和移动端页面开发第8章FlexBox布局(上)
HTML5+CSS3前端入门教程---从0开始通过一个商城实例手把手教你学习PC端和移动端页面开发第8章FlexBox布局
17819 24
|
设计模式 存储 监控
设计模式(C++版)
看懂UML类图和时序图30分钟学会UML类图设计原则单一职责原则定义:单一职责原则,所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。bad case:IPhone类承担了协议管理(Dial、HangUp)、数据传送(Chat)。good case:里式替换原则定义:里氏代换原则(Liskov 
36801 22
设计模式(C++版)
|
存储 编译器 C语言
抽丝剥茧C语言(初阶 下)(下)
抽丝剥茧C语言(初阶 下)
|
机器学习/深度学习 人工智能 自然语言处理
带你简单了解Chatgpt背后的秘密:大语言模型所需要条件(数据算法算力)以及其当前阶段的缺点局限性
带你简单了解Chatgpt背后的秘密:大语言模型所需要条件(数据算法算力)以及其当前阶段的缺点局限性
24874 15
|
机器学习/深度学习 弹性计算 监控
重生之---我测阿里云U1实例(通用算力型)
阿里云产品全线降价的一力作,2023年4月阿里云推出新款通用算力型ECS云服务器Universal实例,该款服务器的真实表现如何?让我先测为敬!
36787 15
重生之---我测阿里云U1实例(通用算力型)
|
SQL 存储 弹性计算
Redis性能高30%,阿里云倚天ECS性能摸底和迁移实践
Redis在倚天ECS环境下与同规格的基于 x86 的 ECS 实例相比,Redis 部署在基于 Yitian 710 的 ECS 上可获得高达 30% 的吞吐量优势。成本方面基于倚天710的G8y实例售价比G7实例低23%,总性价比提高50%;按照相同算法,相对G8a,性价比为1.4倍左右。
|
存储 算法 Java
【分布式技术专题】「分布式技术架构」手把手教你如何开发一个属于自己的限流器RateLimiter功能服务
随着互联网的快速发展,越来越多的应用程序需要处理大量的请求。如果没有限制,这些请求可能会导致应用程序崩溃或变得不可用。因此,限流器是一种非常重要的技术,可以帮助应用程序控制请求的数量和速率,以保持稳定和可靠的运行。
29926 52

热门文章

最新文章