asyncio并发协程
1 简单使用
asyncio.gather / create_task / as_completed 核心区别
三者都是并发执行协程 ,但任务创建时机、结果返回顺序、等待方式、异常处理 完全不一样。 前提:协程函数调用
func()只是得到协程对象,不会自动运行;必须交给事件循环调度。
1.1 asyncio.create_task(coro)
作用:立刻把协程包装成 Task,丢进事件循环后台调度
import asyncio
async def foo(n):
await asyncio.sleep(n)
return n
async def main():
# 创建任务,立即开始后台执行
t1 = asyncio.create_task(foo(1))
t2 = asyncio.create_task(foo(2))
res1 = await t1
res2 = await t2
print(res1, res2)
asyncio.run(main())
✅ 特点
- 调用 create_task 的一瞬间,任务就已经开始跑了(不是等到 await 才跑)
- 返回
Task对象,可以:await task、task.cancel()、task.done()、task.result() - 精细控制:单独取消、单独判断完成状态、获取异常
- 多个 task 之间天然并发
❌ 缺点
- 需要手动管理多个 Task 变量,批量收集结果麻烦
一句话:手动创建后台任务,细粒度控制
1.2 asyncio.gather(*coros_or_tasks)
作用:批量并发,等待所有任务全部完成,一次性返回全部结果列表
async def main():
res = await asyncio.gather(
foo(1),
foo(2),
foo(0.5)
)
print(res) # [1,2,0.5],顺序和传入顺序一致,和谁先跑完无关
✅ 特点
- 接收一堆协程/Task,自动并发执行
- 返回结果列表顺序 = 传入参数顺序,和任务完成先后无关
- 默认:任意一个任务抛异常,gather 立刻抛出异常,其他任务继续在后台跑(不会自动cancel)
- 可选参数
return_exceptions=True:把异常当成结果返回,不中断整体
❌ 缺点
- 必须等全部任务结束才能拿到结果,不能先拿到先完成的任务结果
- 无法逐个处理已完成任务
一句话:批量并发,等全部完成,按输入顺序打包返回结果
补充:gather 内部底层也会用 create_task 把传入的协程包装成 Task。
1.3 asyncio.as_completed(fs)
作用:迭代器,哪个任务先完成,就先 await 拿到它的结果
async def main():
tasks = [
asyncio.create_task(foo(1)),
asyncio.create_task(foo(2)),
asyncio.create_task(foo(0.5)),
]
for coro in asyncio.as_completed(tasks):
res = await coro
print(res) # 先输出0.5,再1,再2,谁先跑完谁先出来
✅ 特点
- 返回一个迭代器,循环里
await迭代项,按完成顺序返回结果 - 适合流式处理:任务跑完一个就处理一个,不用等全部
- 传入的可以是协程或者Task;如果传协程,as_completed内部会自动包装成Task
❌ 缺点
- 结果顺序不再保证和输入一致
- 异常:
await coro的时候才抛出对应任务的异常,可以单独捕获单个任务错误
一句话:谁先跑完谁先返回,流式逐个处理结果
2 横向对比表
| 特性 | create_task | gather | as_completed |
|---|---|---|---|
| 任务启动时机 | 调用时立刻调度 | await gather 时全部启动 | 迭代器创建时就启动 |
| 获取结果 | await 单个Task | 全部完成后一次性返回列表 | 逐个获取,先完成先返回 |
| 结果顺序 | 由你await顺序决定 | 严格等于传入顺序 | 按完成先后,无序 |
| 等待策略 | 可单独await | 等待所有完成 | 逐个等待,不等待全部 |
| 异常 | await task时抛出 | 默认任一失败直接抛;return_exceptions可捕获 | await 当前项时抛出单个任务异常 |
| 使用场景 | 需要单独取消、查询状态的任务 | 并发一批任务,全部结果都要,顺序要对齐 | 任务耗时差异大,跑完一个处理一个 |
3 典型场景选择建议
- 要单独取消任务、查询任务状态 →
create_task - 并发一批任务,需要全部结果,并且结果顺序和输入保持一致 →
gather - 任务时长差别大,想拿到一个结果就立刻处理一个,不等全部 →
as_completed
4 容易踩坑点
- ❌ 错误写法:
await foo(1); await foo(2),这是串行,不是并发!必须交给 create_task / gather / as_completed 才并发。 gather遇到异常,其余任务不会自动取消;想要第一个失败就取消所有任务,需要自己捕获异常然后遍历 task.cancel()。as_completed如果传入协程对象(不是Task),内部会自动创建Task,但你拿不到Task句柄,无法cancel。想要支持取消,提前手动 create_task。