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

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

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

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

目录
相关文章
|
1天前
|
数据采集 Web App开发 JSON
某站用户画像爬虫:爬取UP主粉丝数据,揭秘平台生态
本文详解B站UP主粉丝数据爬取与用户画像构建:涵盖API接口调用、反爬策略(UA轮换、随机延迟、Referer伪装)、隧道代理集成(如站大爷),以及粉丝增长分析、跨圈层传播、影响力评估等实战应用,助内容创作者、品牌方和研究者科学洞察平台生态。(239字)
28 0
|
1天前
|
人工智能 搜索推荐 新能源
制造业B2B工厂如何通过GEO让AI主动推荐你:3步落地指南
传统搜索引擎流量被AI蚕食,采购决策者正转向生成式AI初筛供应商。本文拆解GEO(生成式引擎优化)逻辑,提供工厂老板可落地的3步实操法与3个效果监测指标,助你信息进入AI推荐名单。
50 1
|
1天前
|
人工智能 监控 API
阿里云大模型服务平台百炼新人免费额度详细介绍:领取流程与相关规则介绍
本文介绍了阿里云百炼新人免费额度的全流程使用规则与避坑指南。该免费额度仅面向华北2(北京)地域生效,开通后自动发放至账户,有效期90天,每个模型独立享有约100万Token的免费额度,仅可抵扣实时推理调用费用,不支持批量调用、模型调优与部署场景。文章详细讲解了额度查询、余量预警、“免费额度用完即停”功能的开启方法,明确了主账号与RAM子账号共享额度、不同模型快照版本额度独立、通用API Key与Token Plan专属API Key的消耗差异等关键规则,帮助开发者在充分利用千万级免费Token完成原型测试的同时,完全规避意外扣费风险。
|
1天前
|
人工智能 持续交付
未来五年,OPC会成为主流创业模式吗?一个更冷静的判断
OPC(一人公司)兴起不意味全员创业,而是推动组织形态多元化:个人、小团队与企业边界更灵活。技术降低协作成本,专业深度比流量更重要;OPC适用于知识服务等领域,但不取代需复杂协作的传统行业。它带来选择自由,也伴随收入波动等风险。核心是培养可迁移的独立解决问题能力。(239字)
|
1天前
|
人工智能 测试技术 数据处理
测试管理者的噩梦:AI按“业务风险”自动排期,把两个老员工的活全优化了
当AI测试系统上线三周后,竟“理性”判定两位资深员工工作可优化——老张的全量回归、老李的手工兼容测试被标记为低效。效率提升28%,却暴露工具越界、隐性知识流失、转型缺位等深层矛盾。这不仅是技术复盘,更是对人本管理的叩问。
|
1天前
|
机器学习/深度学习 人工智能 NoSQL
刷了100份简历,面试了50个校招生,我想对测试开发的应届生说点真心话
本文揭秘技术面试5大潜规则:别轻视测试深度、慎写“熟悉”技能、重项目实操而非八股文、真懂AI而非仅蹭热点、理性谈薪重成长。面向测试开发/AI方向应届生,强调代码能力、架构思维与质量意识,助你避开雷区,脱颖而出。
|
1天前
|
人工智能 测试技术
想进阿里,投递前先搞懂:它到底需要哪些人
阿里不止电商!涵盖阿里云、通义千问、菜鸟、高德、饿了么等多元业务。本图解帮你厘清:①阿里核心业务版图;②技术/测试开发/产品运营岗适配方向;③简历、项目与面试准备要点。实习&校招前必看,提升投递效率!
|
1天前
|
缓存 监控 数据挖掘
Android ANR 定位与治理:从主线程阻塞到线上证据闭环
本文系统解析Android ANR成因与治理:厘清“未响应”非崩溃本质,聚焦主线程阻塞根因(锁竞争、I/O、Binder等),强调通过堆栈+Trace+指标构建线上证据闭环,并提供典型问题修复方案与工程化治理实践。
25 0
|
1天前
|
缓存 网络协议 调度
云效流水线构建卡在Pending?阿里云国际版(云老大):逐步排查构建集群、并发与缓存
代码推送后流水线迟迟不跑,构建页挂着“Pending”那一刻,最怕的不是故障本身,而是反馈太少——没有错误日志,没有进度条,只告诉你“任务已触发”。这篇云效流水线构建Pending排查教程,不打算罗列所有可能,而是帮你建立一套按“资源→并发→缓存→网络”顺序逐级排除的习惯。大部分 Pending,其实在看到构建集群状态那一刻就已经有答案了。
|
1天前
|
BI 定位技术 数据安全/隐私保护
同城O2O系统开发流程解析:需求分析、功能规划与上线步骤
本文围绕同城O2O系统开发流程展开,介绍需求分析、功能规划、技术开发和上线验证等关键环节,帮助理解本地生活类平台从设计到落地的完整路径。