Skip to content

协程、awaitable 与 Task

前面的练习里已经出现了 async defgather()create_task()TaskGroup。这一页不再加新写法,只把这些对象分清楚:函数调用后拿到的是什么,事件循环真正调度的又是什么。

coroutine function 和 coroutine object

python
async def load_user(user_id: int) -> dict:
    return {"id": user_id}

coro = load_user(42)

load_user 是 coroutine function,coro 是 coroutine object。

普通函数调用会立刻执行函数体;coroutine function 调用只创建 coroutine object。函数体要等到 coroutine 被 await 或被 Task 调度时才会执行。

awaitable 协议

一个对象能被 await,是因为它实现了等待协议。常见三类 awaitable:

awaitable例子用法
coroutine objectload_user(42)await load_user(42)
Taskasyncio.create_task(load_user(42))await task
Future库内部 I/O 操作返回的结果容器框架内部常见

await 做的事很朴素:如果后面的对象还没结果,当前协程先停下。等结果准备好,再从这一行后面继续。

create_task 的意义

下面代码没有并发:

python
result_a = await fetch("a")
result_b = await fetch("b")

第二个请求要等第一个结束后才开始。

用 Task 后,两件事会尽早开始:

python
task_a = asyncio.create_task(fetch("a"))
task_b = asyncio.create_task(fetch("b"))

result_a = await task_a
result_b = await task_b

create_task() 会把 coroutine 包装成 Task,并把 Task 的第一步排进事件循环。你后面 await task_a 等的是结果,不是才开始执行。

gather 和 TaskGroup

asyncio.gather() 适合同时等一组互不依赖的工作,然后按传入顺序拿结果:

python
results = await asyncio.gather(fetch("a"), fetch("b"), fetch("c"))

TaskGroup 适合放一组应该一起开始、一起收尾的任务:

python
async with asyncio.TaskGroup() as tg:
    a = tg.create_task(fetch("a"))
    b = tg.create_task(fetch("b"))

print(a.result(), b.result())

区别可以先这样记:

场景更推荐原因
简单聚合一组成功结果gather写法短,结果顺序稳定
一组任务属于同一个业务阶段TaskGroup子任务失败时取消兄弟任务,退出时统一处理异常
一组任务要一起收尾TaskGroup代码块结束后不会留下没人管的子任务

Python 3.11 引入 TaskGroup。它的好处很具体:离开 async with 时,这组任务不会悄悄留在后台。

别把 Task 引用丢了

有一种写法很容易出事:

python
asyncio.create_task(write_audit_log())

如果你不保存 Task,也不在某个地方等待它,异常可能变成“Task exception was never retrieved”。事件循环只负责安排它运行,不会替你决定这个后台任务该活多久。

可以这样管起来:

python
background_tasks: set[asyncio.Task] = set()

task = asyncio.create_task(write_audit_log())
background_tasks.add(task)
task.add_done_callback(background_tasks.discard)

如果这组任务属于同一个代码块,直接用 TaskGroup

一次 create_task 后会发生什么

小练习

打开 examples/asyncio_demos/02_concurrent_sleep.py

  1. asyncio.gather() 改成 create_task() 加两次 await
  2. 再把 create_task() 去掉,改成连续 await
  3. 比较三种写法的输出顺序和耗时。

面向学习目的的 Python asyncio 中文教程与 mini_asyncio 教学运行时。