在Python的asyncio库中,asyncio.run()和asyncio.gather()是两个基础且至关重要的工具,但它们扮演着截然不同的角色。简单来说:
asyncio.run()是异步程序的启动器,负责管理事件循环的生命周期。asyncio.gather()是并发任务的聚合器,负责在一系列异步任务启动后,同时运行它们。
下面我们来详细说明。
1. asyncio.run():异步世界的入口
asyncio.run() 是一个高级API,专门用于简化启动和关闭asyncio应用程序的过程。你可以把它看作是运行异步代码的"main"函数。
作用与核心功能
它的工作流程非常固定:
- 创建一个新的事件循环(Event Loop)。
- 将你传入的协程(coroutine)作为任务在新建的循环中运行。
- 等待该任务执行完毕。
- 关闭事件循环,并清理所有资源。
使用场景
- 顶层入口 :在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()并不关心事件循环如何启动或关闭。它的唯一职责是在一个已经运行的事件循环中,将多个异步任务"捆绑"在一起,让它们并发执行。
作用与核心功能
- 接收一个或多个可等待对象(协程、Future等)。
- 将它们包装成任务,并并发地在当前事件循环上调度执行。
- 返回一个特殊的Future对象,当你
await它时,它会一直等待,直到所有传入的任务都完成。 - 其最终结果是一个列表,包含了所有任务的返回值(按传入顺序排列)。
使用场景
- 并发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()就是那个能将这些场景组合起来同时播放的工具。这个特效必须在电影院已经开门、放映机已经启动(事件循环已运行)的前提下才能使用。