首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >以为 asyncio.run() 就是简单的启动?它和 loop.run_until_complete() 的区别让我项目崩了3次

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

原创
作者头像
风一样的男子
发布2026-07-31 11:19:24
发布2026-07-31 11:19:24
1440
举报
文章被收录于专栏:编程教程编程教程

第一个通宵:Jupyter Notebook 里跑崩了

事情发生在一个周一的早上。

数据同事跑过来找我,说训练脚本在 Jupyter Notebook 里跑不起来,报了个错:

代码语言:javascript
复制
RuntimeError: asyncio.run() cannot be called from a running event loop

我看了眼代码,没问题啊——标准的 asyncio 写法:

代码语言:javascript
复制
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()

代码语言:javascript
复制
# 在 Jupyter 里直接这样写
data = await fetch_data()

第一个通宵就这么过去了。我心想:行吧,asyncio.run() 不能在已有循环的环境里调用,记住了。

但我没想到,这只是个开始。

第二个通宵:事件循环已关闭

两周后,我写了一个数据处理流水线,需要依次执行三个异步任务:

代码语言:javascript
复制
async def step1(): ...
async def step2(): ...
async def step3(): ...

# 我的写法
asyncio.run(step1())
asyncio.run(step2())
asyncio.run(step3())

跑完 step1,到 step2 的时候报错了:

代码语言:javascript
复制
RuntimeError: Event loop is closed

我懵了。每个 step 单独跑都没问题,串起来就崩?

查了源码才明白:**asyncio.run() 每次调用,都会走一遍"创建→运行→关闭"的完整生命周期**。

第一次 asyncio.run(step1()) 跑完,事件循环就被关闭了。第二次调用的时候,它试图创建一个新的循环,但之前的关闭动作留下了残留状态。

asyncio.run() 这个函数的设计初衷,是让你在程序的最高层调用一次——作为整个异步程序的入口点。它不是给你反复调用的。

正确的写法应该是:

代码语言:javascript
复制
async def main():
    await step1()
    await step2()
    await step3()

asyncio.run(main())  # 只调用一次

或者,如果你确实需要在同一个事件循环里执行多个独立操作,用 loop.run_until_complete()

代码语言:javascript
复制
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

又过了两周,代码上线了。我在一个已有的异步服务里,调用了一个第三方库的函数。

那个函数里面是这样写的:

代码语言:javascript
复制
# 第三方库的代码
def do_something_sync():
    loop = asyncio.get_event_loop()
    loop.run_until_complete(some_async_task())

我的服务本来就在事件循环里跑着,调用了这个函数之后——

代码语言:javascript
复制
RuntimeError: This event loop is already running

run_until_complete() 的规则是:它只能在没有运行中的循环时被调用。如果循环已经在跑了,你再调 run_until_complete() 就会冲突。

这就好比:车已经发动了,你非要再拧一次钥匙。

那怎么办?如果循环已经在运行,你应该用 create_task() 或者 await,而不是再调 run_until_complete()

代码语言:javascript
复制
# 在已经运行的事件循环里
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() 的简化源码就全明白了:

代码语言:javascript
复制
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(),调用一次,简洁干净:

代码语言:javascript
复制
async def main():
    # 你的异步逻辑
    pass

if __name__ == "__main__":
    asyncio.run(main())

场景二:在 Jupyter / IPython 里写代码

别用 asyncio.run(),直接用 await

代码语言:javascript
复制
# Jupyter 里直接 await
result = await my_async_function()

场景三:需要依次执行多个独立任务,复用同一个循环

用手动管理的方式:

代码语言:javascript
复制
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

代码语言:javascript
复制
with asyncio.Runner() as runner:
    runner.run(task1())
    runner.run(task2())
    runner.run(task3())

场景四:在已有的异步函数里调用另一个异步函数

直接用 await,别搞花活:

代码语言:javascript
复制
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 删除。

目录
  • 第一个通宵:Jupyter Notebook 里跑崩了
  • 第二个通宵:事件循环已关闭
  • 第三个通宵:在已有的循环里调用 run_until_complete
  • 所以到底有什么区别?
  • 源码里藏着答案
  • 实战:什么时候用什么?
  • 三个通宵换来的教训
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档