协程、awaitable 与 Task
前面的练习里已经出现了 async def、gather()、create_task() 和 TaskGroup。这一页不再加新写法,只把这些对象分清楚:函数调用后拿到的是什么,事件循环真正调度的又是什么。
coroutine function 和 coroutine object
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 object | load_user(42) | await load_user(42) |
| Task | asyncio.create_task(load_user(42)) | await task |
| Future | 库内部 I/O 操作返回的结果容器 | 框架内部常见 |
await 做的事很朴素:如果后面的对象还没结果,当前协程先停下。等结果准备好,再从这一行后面继续。
create_task 的意义
下面代码没有并发:
result_a = await fetch("a")
result_b = await fetch("b")第二个请求要等第一个结束后才开始。
用 Task 后,两件事会尽早开始:
task_a = asyncio.create_task(fetch("a"))
task_b = asyncio.create_task(fetch("b"))
result_a = await task_a
result_b = await task_bcreate_task() 会把 coroutine 包装成 Task,并把 Task 的第一步排进事件循环。你后面 await task_a 等的是结果,不是才开始执行。
gather 和 TaskGroup
asyncio.gather() 适合同时等一组互不依赖的工作,然后按传入顺序拿结果:
results = await asyncio.gather(fetch("a"), fetch("b"), fetch("c"))TaskGroup 适合放一组应该一起开始、一起收尾的任务:
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 引用丢了
有一种写法很容易出事:
asyncio.create_task(write_audit_log())如果你不保存 Task,也不在某个地方等待它,异常可能变成“Task exception was never retrieved”。事件循环只负责安排它运行,不会替你决定这个后台任务该活多久。
可以这样管起来:
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:
- 把
asyncio.gather()改成create_task()加两次await。 - 再把
create_task()去掉,改成连续await。 - 比较三种写法的输出顺序和耗时。