asyncio.run() vs asyncio.gather():启动器与聚合器, 为何Jupyter notebook里能直接使用await?

在Python的asyncio库中,asyncio.run()asyncio.gather()是两个基础且至关重要的工具,但它们扮演着截然不同的角色。简单来说:

  • asyncio.run()异步程序的启动器,负责管理事件循环的生命周期。
  • asyncio.gather()并发任务的聚合器,负责在一系列异步任务启动后,同时运行它们。

下面我们来详细说明。


1. asyncio.run():异步世界的入口

asyncio.run() 是一个高级API,专门用于简化启动和关闭asyncio应用程序的过程。你可以把它看作是运行异步代码的"main"函数。

作用与核心功能

它的工作流程非常固定:

  1. 创建一个新的事件循环(Event Loop)。
  2. 将你传入的协程(coroutine)作为任务在新建的循环中运行
  3. 等待该任务执行完毕。
  4. 关闭事件循环,并清理所有资源。

使用场景

  • 顶层入口 :在Python脚本(.py文件)的最高层,用于启动整个异步程序。
  • 简单脚本 :当你只需要运行一个顶层的异步函数时,asyncio.run() 是最简单直接的方式。

重要限制

  • 一个程序中通常只调用一次。
  • 不能在一个已经运行的事件循环中调用它。
特例:Jupyter Notebook / IPython 环境

这个限制最常见的场景就是在Jupyter Notebook或IPython这样的交互式环境中。

  • 为什么会报错?

    Jupyter为了实现单元格的异步执行和交互功能,其自身已经在后台启动并管理着一个事件循环。当asyncio.run()试图创建并启动一个 的循环时,它会检测到当前线程已存在一个循环,因此会立即抛出RuntimeError以防止冲突。

  • 在Jupyter中应该怎么做?

    利用Jupyter已经为您准备好的事件循环即可。现代Jupyter环境支持"顶层await",这意味着您可以直接在代码单元格中await一个协程,而无需任何启动器。

    python 复制代码
    # 在Jupyter单元格中,这样是正确的:
    import asyncio
    
    async def my_coroutine():
        await asyncio.sleep(1)
        return "Done!"
    
    result = await my_coroutine()
    print(result)

代码示例

这是一个典型的独立Python脚本:

python 复制代码
# a_simple_script.py
import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    print(f"started at {time.strftime('%X')}")
    await say_after(1, 'hello')
    await say_after(2, 'world')
    print(f"finished at {time.strftime('%X')}")

# 使用 asyncio.run() 来启动整个 main 协程
if __name__ == "__main__":
    asyncio.run(main())

# 输出:
# started at 10:00:00
# hello
# world
# finished at 10:00:03

2. asyncio.gather():并发执行的魔术师

run()不同,gather()并不关心事件循环如何启动或关闭。它的唯一职责是在一个已经运行的事件循环中,将多个异步任务"捆绑"在一起,让它们并发执行。

作用与核心功能

  1. 接收一个或多个可等待对象(协程、Future等)。
  2. 将它们包装成任务,并并发地在当前事件循环上调度执行。
  3. 返回一个特殊的Future对象,当你await它时,它会一直等待,直到所有传入的任务都完成。
  4. 其最终结果是一个列表,包含了所有任务的返回值(按传入顺序排列)。

使用场景

  • 并发IO操作 :当你需要同时执行多个网络请求、数据库查询或文件读写时,gather()是实现并发、节省时间的关键。
  • 任务分组:将一组相关的异步操作作为一个单元来管理。

代码示例

让我们用gather()来优化上面的例子,让"hello"和"world"同时发生:

python 复制代码
# a_concurrent_script.py
import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    print(f"started at {time.strftime('%X')}")

    # 将两个协程作为任务并发执行
    await asyncio.gather(
        say_after(1, 'hello'),
        say_after(2, 'world')
    )

    print(f"finished at {time.strftime('%X')}")

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

# 输出:
# started at 10:00:00
# hello          <-- 1秒后打印
# world          <-- 2秒后打印
# finished at 10:00:02 <-- 总耗时取决于最长的那个任务

注意,总耗时约为2秒,而不是像第一个例子中的3秒,因为两个say_after任务是并发运行的。


3. 核心区别总结

特性 asyncio.run() asyncio.gather()
角色 启动器 (Starter) 聚合器 (Aggregator)
层级 顶层入口,管理整个程序 内部工具,用于并发执行子任务
事件循环 创建、运行并销毁的事件循环 在一个已存在的事件循环中运行
调用方式 asyncio.run(coro) await asyncio.gather(coro1, coro2, ...)
常见位置 if __name__ == "__main__": 在一个async def函数内部

形象比喻

  • asyncio.run()电影院的经理。他的工作是:开门营业(创建循环),播放当天的电影(运行主协程),等电影放完(等待完成),最后关门谢客(关闭循环)。
  • asyncio.gather() 是电影中的多屏特效 。导演(你的代码)想要在同一时间展示多个场景(并发任务),gather()就是那个能将这些场景组合起来同时播放的工具。这个特效必须在电影院已经开门、放映机已经启动(事件循环已运行)的前提下才能使用。
相关推荐
EEG小佬2 小时前
Jupyter选择内核时如何找到虚拟环境
ide·python·jupyter
文人sec2 小时前
使用python-pandas-openpyxl编写运营查询小工具
开发语言·python·pandas
hu_yuchen3 小时前
问卷系统自动化测试报告
软件测试·python
这儿有一堆花3 小时前
C语言递归宏详解
c语言·开发语言·c++
csbysj20203 小时前
C 标准库 - `<ctype.h>`
开发语言
郝学胜-神的一滴3 小时前
计算机图形中的法线矩阵:深入理解与应用
开发语言·程序人生·线性代数·算法·机器学习·矩阵·个人开发
百锦再3 小时前
第8章 模块系统
android·java·开发语言·python·ai·rust·go
m0_591338913 小时前
day8鹏哥C语言--函数
c语言·开发语言·算法
oplp3 小时前
回过头来重新对C语言进行深度学习(一)
c语言·开发语言