本文目录
def fetch(): return 1 调用即执行;async def fetch(): return 1 调用 不 执行函数体——你得到 协程 对象,像未启动的子程序。必须交给 事件循环(经 asyncio.run 或 Task)才会从入口往下跑;遇到 await 时 暂停,把 控制权 交还 调度,I/O 或 Future 就绪后再从暂停处 恢复。这一章只谈 协程 机制本身,不谈 gather 等模式(下一章)。
async def:协程函数 vs 协程对象
async def 定义 协程函数(coroutine function)。调用它产生 协程 对象(coroutine object),不 运行体内代码:
import asyncio
import inspect
async def greet(name: str) -> str:
print(f"inside greet({name!r})")
return f"hi, {name}"
coro = greet("Ada")
print(type(coro).__name__) # coroutine
print(inspect.iscoroutine(coro)) # True
print(inspect.iscoroutinefunction(greet)) # True
coro.close()对比普通函数:
def sync_greet(name: str) -> str:
print(f"sync {name}")
return f"hi, {name}"
print(sync_greet("Bob"))
# sync Bob
# hi, Bob在同步上下文里只写 greet("Ada") 而不 await,会得到 coroutine 对象并可能 RuntimeWarning: coroutine 'greet' was never awaited。这不是语法糖能自动修复的——协程 必须被 调度。
生命周期:创建 → 调度 → 关闭
| 阶段 | 发生什么 |
|---|---|
coro = f() | 分配 协程 帧,代码未执行 |
Task / asyncio.run | loop 驱动到第一个 await 或 return |
await 可等待对象 | 暂停,保存局部变量与指令位置 |
| 可等待对象 done | 恢复,继续下一条语句 |
return / 未捕获异常 | 协程 关闭,Task set_result 或 set_exception |
import asyncio
async def trace() -> None:
print("1 enter")
await asyncio.sleep(0)
print("2 resumed")
await asyncio.sleep(0)
print("3 end")
async def main() -> None:
await asyncio.create_task(trace())
asyncio.run(main())输出:
1 enter
2 resumed
3 end每次 await asyncio.sleep(0) 都是 让出点:注册定时器,事件循环 跑其它 ready 回调,到期后再 恢复 打印下一行。帧上的局部变量在 暂停 期间保留——像线程栈,但仍在同一条 OS 线程上。
await 的语义:挂起与恢复
await expr 要求 expr 是 Awaitable(协程、Task、Future,或实现 __await__ 的对象):
import asyncio
async def inner() -> int:
await asyncio.sleep(0.01)
return 7
async def outer() -> None:
print("before await")
x = await inner()
print("after await", x)
asyncio.run(outer())时间线(概念):
outer 开始 → 打印 before → 进入 inner → inner await sleep → outer 与 inner 均挂起
→ sleep 到期 → inner return 7 → outer 恢复 → 打印 after await 7await 做两件事:把当前 协程 登记为「子结果未就绪的等待者」;把 控制权 还给 事件循环。子结果就绪后,loop 调度 父 协程 从 await 表达式处继续,并把子结果作为表达式值。
若子结果 已 done(例如已完成的 Future),await 不挂起,直接取值——这对性能与语义都重要:不要假设每次 await 都会让出。
只有 awaitables:普通调用不会变异步
import asyncio
import time
def blocking() -> int:
time.sleep(0.1)
return 1
async def wrong_idea() -> int:
# await blocking() # TypeError
return blocking() # 能返回 1,但阻塞整个 loop
async def right() -> int:
return await asyncio.to_thread(blocking)
asyncio.run(right())写 async def 不会把调用的同步函数变非 阻塞;只有 await 真正的 async I/O 或 to_thread 等桥接,才不占 调度 线程。
协程必须被调度:三种启动方式
import asyncio
async def work(tag: str) -> None:
print(tag)
# 1) 脚本入口
asyncio.run(work("via-run"))
# 2) create_task 并发
async def main() -> None:
asyncio.create_task(work("via-task"))
await asyncio.sleep(0)
asyncio.run(main())
# 3) 直接 await —— 串行
async def serial() -> None:
await work("first")
await work("second")
asyncio.run(serial())asyncio.run(main_coro) 内部把 main_coro 包成 Task 并跑到 done——这是 协程 与 事件循环 的默认桥梁。create_task 则在已有 async 上下文里 并发 启动子 协程;只 await 则不并发,只是嵌套调用。
漏 调度 的典型 bug:
import asyncio
async def orphan() -> None:
print("never scheduled")
coro = orphan()
coro.close()与生成器:像,但协议不同
CPython 里 协程 对象基于生成器机制扩展,但 语义 不同:
| 生成器 | 协程 | |
|---|---|---|
| 定义 | def + yield | async def + await |
| 驱动 | .send() / for | 仅 loop 通过 await |
| 返回值 | StopIteration.value | 写入 Task / Future |
| 目的 | 惰性迭代 | I/O 协作 调度 |
def gen():
yield 1
yield 2
g = gen()
print(next(g), next(g)) # 1 2
async def co():
await asyncio.sleep(0)
return 3
async def use_co() -> None:
print(await co())
asyncio.run(use_co()) # 3async def 里写 yield 会变成 异步生成器(async for 协议),不是普通 协程。学习 asyncio 时只需记住:await 交还 控制权 给 事件循环,不是交还给调用 async def 的同步函数。
协程帧里保存什么
暂停 时,解释器保存:局部变量、指令偏移、内部 await 栈(嵌套 await 时)。因此 协程 可以 await 深层调用,而不额外占 OS 线程:
import asyncio
async def depth(n: int) -> int:
if n <= 0:
return 0
await asyncio.sleep(0)
return 1 + await depth(n - 1)
async def main() -> None:
print(await depth(4)) # 4
asyncio.run(main())每层 await sleep(0) 允许 loop 穿插其它 Task;递归过深仍占内存——极端场景应改循环。
async with 与 async for:语法糖里的 await
async with 调用对象的 __aenter__ / __aexit__(二者必须是 可等待 的);async for 驱动 异步迭代器 的 __anext__。它们仍是 协程 代码路径上的 await,只是语法隐藏了:
import asyncio
class AsyncCtx:
async def __aenter__(self) -> str:
await asyncio.sleep(0)
return "entered"
async def __aexit__(self, *args: object) -> None:
await asyncio.sleep(0)
print("left")
async def main() -> None:
async with AsyncCtx() as v:
print(v)
asyncio.run(main())
# entered
# left读 trace 时把 async with 展开成两次 await 有助于理解 调度 点。
未 awaited 协程与 GC
协程 对象若既未 Task 也未 await,析构时可能警告。显式 coro.close() 可提前释放帧并注入 GeneratorExit。在框架里一般由 Task 生命周期管理;手写脚本要注意别「创建 coroutine 又丢弃」。
与 Task、Future 的衔接
create_task(coro) 把 协程 绑到 Task(Future 子类)。协程 return v → Task set_result(v);未捕获异常 → set_exception。await task 就是 await 那个 Future 侧的结果。因此 协程 章节与 Future 章节是同一枚硬币的两面:协程 写逻辑,Future/Task 写「完成态如何通知 loop」。
常见误解
误解一:「async def 在后台线程跑。」 仍在 loop 线程,除非 to_thread。
误解二:「函数里调 async 函数不用 await。」 得到未执行 coroutine,逻辑错误。
误解三:「await 等于 yield。」 await 针对 Awaitable 与 loop 调度;yield 针对迭代协议。
误解四:「协程一定比线程轻,所以可以无限创建。」 轻量但有 调度 与内存成本;百万 idle Task 要慎重。
误解五:「asyncio.run 里可以再 asyncio.run。」 嵌套 asyncio.run 在已有 running loop 会失败;用 await 调子 协程。
嵌套 await 与「谁让出」
import asyncio
async def a() -> int:
print("a: start")
await asyncio.sleep(0.01)
print("a: end")
return 1
async def b() -> int:
print("b: start")
x = await a()
print("b: got", x)
return x + 1
async def c() -> None:
print("c: start")
y = await b()
print("c: got", y)
asyncio.run(c())输出顺序固定为 c start → b start → a start → a end → b got 1 → c got 2。**await a()** 在 b 的帧里 暂停 b,a 跑完 b 才继续——调用栈逻辑仍是深度优先,只是 a 在 await sleep 时把 控制权 交给 事件循环,允许其它 Task 插入(本例没有其它 Task)。
若在同一层用 create_task(a()) 而非 await a(),b 不会等 a 的返回值才往下走——启动 与 等待 再次分离(下一章重点)。
协程的 close、throw 与调试
协程 对象有 .close()、.throw(exc)(少用)。.close() 在 GeneratorExit 路径上清理;.throw 向 暂停 点注入异常。框架代码偶尔用;日常 Task.cancel() 更常见。
调试「卡在哪」:看 Task 名、栈里最后一个 await 所在的库(例如等 socket、等 Semaphore)。asyncio.current_task() 在 协程 内取当前 Task:
import asyncio
async def whoami() -> None:
t = asyncio.current_task()
print(t.get_name() if t else None)
async def main() -> None:
asyncio.create_task(whoami(), name="inner")
await asyncio.sleep(0)
asyncio.run(main())
# inner从同步心智迁移:四条规则
async def调用不执行 —— 先create_task或await。await是唯一协作让出点(外加async with/for里的隐式 await)。- 普通函数调用仍可能阻塞 —— 与是否
async def无关。 - 并发来自多个 Task 交错,不是来自
async def语法本身。
用这四条审阅现有代码,比背 API 表更快发现「假 async」。
与线程模型对照(仅一点)
线程里「等结果」常 阻塞 在 future.result();协程 里用 await,等的是 Future done 事件,线程可 调度 其它 Task。不要把 await 理解成「把线程挂起」——挂起的是 协程,不是 OS 线程;loop 线程仍在 调度 ready 队列。
生成器 yield from 与 async 的分叉(辅助)
早期 asyncio 用 @asyncio.coroutine + yield from 写 协程;3.5+ async def / await 成为标准。旧语法已移除。今天若在读老项目,看到 yield from asyncio.sleep(...), mentally 替换成 await asyncio.sleep(...) 即可——控制权 交还 loop 的语义不变。
同一函数内多个 await 点
import asyncio
async def pipeline() -> str:
print("step1")
await asyncio.sleep(0.01)
print("step2")
await asyncio.sleep(0.01)
print("step3")
return "done"
async def interleave() -> None:
asyncio.create_task(pipeline())
for i in range(3):
print("other", i)
await asyncio.sleep(0.005)
asyncio.run(interleave())pipeline 与 interleave 的 await 点交错——证明 单线程 上多 Task 靠 事件循环 调度 穿插,而非并行执行 pipeline 与 interleave 的字节码。
async def 不是「异步返回值类型」
import asyncio
async def f() -> int:
return 42
async def main() -> None:
x = await f()
print(type(x), x)
asyncio.run(main())
# <class 'int'> 42返回类型仍是 int,不是「Future[int]」。Future 是 调度 层对象;协程 return 的值经 Task 写入 Future,await 侧看到普通 Python 值。
协程对象在内存里是什么
协程 对象是 CPython 里的 coroutine 类型,基于生成器扩展,带 cr_frame、**cr_await** 等字段。**await x** 在字节码层调用 GET_AWAITABLE,再驱动 awaitable 的 __await__ 协议。你不必背字节码,但应知道:暂停 保存的是 帧,不是新线程——因此 协程 切换成本远低于 OS 线程,也受 GIL 约束在同一进程内。
只在 async 函数里能写 await
**await** 语法仅允许出现在 async def 函数体中(及 async with/for 脱糖后的内部)。在普通 def 里 await 是语法错误。这强制「会 挂起 的代码」与「同步代码」在语法层分离,减少误把 阻塞 调用写进 async 路径的概率——尽管仍拦不住 time.sleep。
协程不返回 Future,Task 才包 Future
import asyncio
async def g() -> int:
return 1
coro = g()
print(asyncio.isfuture(coro)) # False
t = asyncio.create_task(g())
print(asyncio.isfuture(t)) # True
t.cancel()**g()** 是 协程;只有 create_task(或 ensure_future)才得到 Future/Task 语义的对象供 调度 追踪。
并发不是 async def 的语法自带
import asyncio
async def work(n: int) -> int:
await asyncio.sleep(0.1)
return n
async def fake_parallel() -> None:
# 没有并发:顺序 await
print(await work(1))
print(await work(2))
async def real_parallel() -> None:
t1 = asyncio.create_task(work(1))
t2 = asyncio.create_task(work(2))
print(await t1, await t2)
asyncio.run(real_parallel())**async def** 只定义 可暂停函数;并发 来自多个 Task 交错 调度,来自 gather 汇合,不来自关键字本身。
yield from 时代遗留(了解即可)
Python 3.4–3.10 过渡期存在 @types.coroutine / yield from 写法驱动 Future。今天统一 async def / await。await 可读性更好,且与 async generator、async with 同一套语法。读老博客若见到 yield from asyncio.sleep(0),等价于 modern await asyncio.sleep(0)。
异步生成器:第三个 async 物种
**async def** 里写 yield 得到 异步生成器,用 async for 消费,不是 普通 协程:
import asyncio
async def agen():
yield 1
yield 2
async def main() -> None:
async for x in agen():
print(x)
asyncio.run(main())
# 1
# 2async for 内部多次 **await __anext__。别把 async generator 当成 create_task 的对象——它用于 流式 数据,不是「跑完一个 Future 结果」。
调试:coroutine was never awaited
泄漏 协程 对象会 RuntimeWarning。原因常见:忘了 await、忘了 create_task、或条件分支里创建了 coroutine 却未 调度。修复:保证每个 async def() 调用路径最终 await 或 create_task + 生命周期管理,或 coro.close()。
与 Future 章节的闭合
协程 是写逻辑的单位;Future/Task 是 调度 与完成态的单位。await 把 协程 绑在 Future 的 pending 上;set_result 让 协程 恢复。事件循环 在两者之间轮转 就绪队列。三章合读:loop 轮转、Future 状态、协程 暂停——asyncio 的主干就齐了。
调用栈与 Task 边界
await 嵌套再深,OS 线程仍一条;调试器里看到多层 coroutine 帧是正常的。create_task 切开 调用栈:parent 与 child 并发,栈不再父子包含——parent 在 await gather 等 child 的 Future。
return 与 StopIteration(实现细节)
协程 return v 在内部触发 StopIteration(v),Task 捕获并把 v set_result 到 Future。裸 协程 用 .send(None) 驱动时可见 StopIteration;日常 await 只看到返回值。
async 函数是一等公民
协程函数 可赋值、当参数、闭包捕获——与 def 相同,只是调用产物是 协程 而非立即执行:
import asyncio
from typing import Callable, Awaitable
async def apply(f: Callable[[], Awaitable[int]]) -> int:
return await f()
async def main() -> None:
async def inner() -> int:
return 7
print(await apply(inner))
asyncio.run(main())
# 7类型标注:Coroutine vs Awaitable
**async def f() -> T** 调用得 Coroutine[Any, Any, T];参数常标注 Awaitable[T] 以接受 Task/Future。静态类型帮助区分「同步 callable」与「要 await 的对象」。
练习式读码:从 main 向下追 await
读任意 async 项目:从 async def main 或 asyncio.run 入口开始,列出每个 create_task 与 await。标记每个 await 等的是 sleep、I/O Future 还是 Task——练几次后 pending 卡死的位置会一眼可见。
顶层 await 与 REPL
CPython 3.8+ REPL 支持 顶层 await,内部替你 get_event_loop 驱动 协程——方便实验,不是生产入口。脚本仍用 asyncio.run。
协程与 functools.partial
partial(async_fn, arg) 仍返回 协程函数 调用后的 协程;调度 前仍是 coro = partial(...)( 或 create_task(partial(...)())。不要 partial(create_task, coro) 混淆 启动 时机。
多层 async def 与测试
测试常 await func_under_test() 而非 asyncio.run 包每个用例——pytest-asyncio 提供 loop fixture。无论哪种,协程 必须被 驱动,否则 never awaited。
错误:在 sync 里 asyncio.run 嵌套
import asyncio
async def inner() -> int:
return 1
def outer() -> int:
return asyncio.run(inner())
# outer() # 可用
# async def bad():
# asyncio.run(inner()) # 在 running loop 里嵌套 run → 错asyncio.run 独占「创建-运行-销毁 loop」;在 async 函数里应 await inner()。
协程泄漏的代价
未 await 的 协程 持有帧与闭包引用,可能泄漏大对象。create_task 后不 await 同理——Task 引用 协程 帧直到 done。fire-and-forget 必须 强引用 + 异常 logging 或 TaskGroup 汇合。
与 Future:一句话
协程 写「怎么做」;Future 写「结果何时就绪」;Task 把二者绑在一起交给 事件循环 调度。
协程局部变量在 await 前后存活
import asyncio
async def demo() -> None:
big = list(range(1000))
await asyncio.sleep(0)
print(len(big))
asyncio.run(demo())
# 1000await 暂停 不释放 帧 局部变量——长生命周期 Task holding 大对象会增加内存。必要时 del 或缩小 Task 范围。
async def 与staticmethod / classmethod
@staticmethod async def 合法:调用 Cls.static() 得 协程,仍须 await/Task。classmethod 同理。实例方法 async def method(self) 调用 obj.method() 得 协程,不是 coroutine function 再包一层。
理解 await 协议
自定义 Awaitable 实现 __await__ 返回 iterator;yield from 或 asyncio 内部 Task 驱动。Future 的 __await__ 在 pending yield 控制权给 Task,done 时 return 结果。扩展库时用 asyncio.Future 子类较少,多数 compose 现有 Future。
对比:同步 def 里调 async —— 边界情况
import asyncio
async def aio() -> int:
return 5
def sync_caller() -> int:
return asyncio.run(aio())
print(sync_caller()) # 5sync 入口 asyncio.run 是桥梁;async 入口 await 是桥梁——不要混用导致 嵌套 loop。
协程帧深度与 asyncio 并发上限
单进程 thousands Task 可行,但每个 Task 一个 协程 帧 + Future 开销。await 点多 ≠ 自动 并发;create_task 数量与 Semaphore 才定义 in-flight 上限。性能调优看 Task 数、pending Future 数、loop slow callback,不是 async def 个数。
与 事件循环、Future 三句口诀
loop 轮转 ready;Future pending 等 set_result;协程 await 把自身 挂起 到 Future 上。背口诀不如画一张你自己的请求时序图——画完 async 就不神秘。
附录:async def 调用链逐步表
| 代码 | 得到什么 | 是否执行体 | 下一步 |
|---|---|---|---|
async def f | 协程函数 | — | 调用 f() |
f() | 协程对象 | 否 | Task 或 await |
create_task(f()) | Task | 是(异步) | await task 可选 |
await f() | 结果 T | 是(异步) | — |
| `asyncio.run(f()) | 结果 T | 是 | 进程级入口 |
表背下来,80% 的「协程没跑」问题可秒定位。
多个 await 同一 coroutine 对象?
import asyncio
async def once() -> int:
print("run")
return 1
async def main() -> None:
c = once()
# await c; await c # 第二次 await 同一 coroutine 对象 → 错
await asyncio.gather(once(), once())
asyncio.run(main())
# run
# run协程 对象 一次性——await 消耗它。要两次结果,调两次 once() 或 reuse Task(Task 可 await 一次取结果,done 后 await 仍得同值 via Future 语义)。
async 闭包与循环变量(经典坑)
import asyncio
async def main() -> None:
tasks = []
for i in range(3):
async def work() -> None:
print(i) # 闭包捕获 i,循环结束 i=2
tasks.append(asyncio.create_task(work()))
await asyncio.gather(*tasks)
asyncio.run(main())
# 2 2 2与 sync 闭包相同;用 default arg async def work(i=i) 修复——async 不免疫 LEGB 闭包陷阱。
协程与装饰器
@decorator 包 async def 时,装饰器须返回 async def 或 await 兼容的 callable。@sync_decorator 包 async 常破坏 await 链——要用 async def wrapper 并在 wrapper 里 await func()。
从 def 迁移到 async def 的检查单
- 每个
async def调用是否 await 或 create_task? - 是否仍有 sync blocking I/O?
- 并发 是否 create_task/gather,而非假 async 顺序 await?
- 测试是否用 pytest-asyncio 或 asyncio.run 驱动?
与 Task、Future 的最终关系图(文字)
协程函数 → 调用 → 协程对象 → create_task → Task(Future) → loop 驱动 → await 点 挂起 → 底层 Future set_result → 协程 恢复 → return → Task done。整条链上 协程 是「可暂停函数体」;Future 是「完成通知」;loop 是「谁在什么时候运行哪一段」。
读者自查:协程概念是否真懂
async def 调用为何不打 print?await 与 return 区别?为何 time.sleep 在 async def 仍 阻塞?create_task 与 await coro() 并发差在哪?协程 对象能否 await 两次?——能答即掌握本章。
与事件循环、Future 的交叉索引
协程 await → Future pending → loop 调度 其他 Task → set_result → 协程 恢复。async def alone 不够;三角关系缺一角就 卡住。
常见面试式追问(自测)
- 协程 与 线程 栈谁轻?协作 与 抢占 区别?
- async generator 与 coroutine 区分?
- asyncio.run 能否在 FastAPI 路由里再调?
答案都在前文;自测加深 机制 记忆,非背题。
写库 vs 写应用
库 暴露 async def 或 Awaitable;应用 用 create_task/gather 组织 并发。库 内避免 阻塞;应用 选 Semaphore/timeout 策略。角色不同,协程 用法侧重不同。
最后一轮机制巩固:await 展开成两步
概念上 x = await aw 等价于:
- 若 aw 已 done,x = 结果(或 抛 异常)。
- 若 pending,挂起 当前 协程,登记 aw;aw done 后 恢复,x = 结果。
async def 编译器生成 状态机 字节码实现这两步——你写 await,解释器维护 帧 与 指令偏移。
为何普通函数不能 await
await 需要 当前帧 是 协程 帧,以便 挂起。def 帧不支持 yield from awaitable 的 协程 协议——语法层禁止,减少 阻塞 def 误用 await 的混乱。
协程教学常见一张图(文字版)
调用 async 函数 → coroutine 对象 → create_task → Task
↓
事件循环 ready 队列
↓
运行到 await → 挂起
↓
Future done → 恢复 → return背图不如自己 debug 单步跟一次 asyncio.run(main())——机制 变 肌肉记忆。
合并阅读建议
协程 章回答「async def 调用后为何不跑、await 干什么」;与 Future 章连读:await 的就是 Future/Task 的 pending。**与 事件循环 章连读:await 把 控制权 交还 loop。三章合读 后再看 模式 章的 create_task/gather,顺序最省认知负荷。
手写测试:coroutine 必须被驱动
import asyncio
import warnings
async def silent() -> None:
pass
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
c = silent()
del c
# 可能 RuntimeWarning: coroutine was never awaited实验提醒:协程 不是「调用即运行」——调度 是显式义务。
async/await 与语言演化
async def / await 进入语法后,生成器风格 coroutine 退出历史。协程 成为与 def、class 并列的一等语法类别——但 运行时 仍依赖 事件循环 调度,语法便利不替代 机制。
结束语前的检查表
- async def() 得到什么?为何不执行?
- await 谁能 await?
- create_task 与 await coro() 区别?
- 协程 与 async generator 如何分?
- asyncio.run 与 await 在 async main 里如何分工?
五项打勾,协程 章可以收工。
深度:async def 编译结果
async def 函数体被编译为含 CO_COROUTINE 标志的 code object;调用时 types.coroutine 包装为 coroutine。await 对应 GET_AWAITABLE + YIELD FROM 路径(概念上)。return 在 3.3+ coroutine 用 StopIteration(value) 传值给 Task。
协程与 Iterator 协议边界
协程 不是 Iterator——不能 for x in coro();须 await 或 Task。async generator 才有 __aiter__。混淆 coroutine 与 generator 是初学者高频 TypeError 来源。
结束补充
协程 章与 Future 章、loop 章、模式 章构成 Python asyncio 最小完备集;缺任何一角,pending hung 或 阻塞 loop 时都会无从查起。
协程章节完整复述(一页纸)
async def 定义 协程函数,调用得 协程对象,不自动执行;须 asyncio.run 或 create_task 调度;await 仅对 Awaitable,在 pending 时 挂起 并把 控制权 交还 事件循环;return 经 Task 写入 Future;async generator 与 coroutine 不同。背下这段,never awaited 与 阻塞 loop 的 80% 根因可快速归类。
与 JavaScript async function 的单向对比(一句)
async def 调用也不执行体,与 JS async function() 类似;但 Python 协程 必须被 asyncio loop 驱动,没有 浏览器 那种 自动 microtask 队列——Task/asyncio.run 是显式 调度 入口。
温故:协程相关的四个「何时」
何时产生 coroutine? async def() 调用。何时执行? Task schedule 或 await 驱动。何时挂起? await pending Awaitable。何时结束? return / 未捕获异常 / cancel。四个 何时 串起来,就是 单条 Task 的生命周期叙事。
温故:async def 与 def 的对照
| def | async def | |
|---|---|---|
| 调用 | 立刻执行体 | 得 coroutine,不执行 |
| 等待 I/O | 阻塞 线程(sync) | await 挂起 协程 |
| 并发 | 线程/进程 | Task + loop |
| 返回值 | 直接 T | await 后得 T |
async def 不是 def 的 超集——是 不同 调度 契约下的函数形式。
收束
协程 是 async def 调用后的「可暂停执行体」;await 是暂停按钮;Task 与 asyncio.run 是播放按钮。整章若只记一句,记:调用 async 函数不会跑,调度才会跑。
建议在 REPL 里 async def f(): return 1 后 f()、type(f())、await f()(顶层)各试一次,把 协程对象 与 调度 的因果关系刻进肌肉记忆。写 async 代码时先问「这一行是 启动、等待 还是 阻塞」,比死记 API 更少踩坑。下一章 模式 会把 Task 与 gather 放到真实 并发 场景里。通读后应能区分 协程对象 与 Task 何时创建、何时 await。这是 asyncio 语法层最容易混淆、也最值得花时间夯实的一点。建议对照本文 async def 调用链逐步表 做一次自测默写。能不看表说出从 f() 到 await task 的每一步,本章目标即达成。与 Future 章合读时,重点盯住 await 如何把 协程 挂起 在 pending 的 Future 上——这是 async/await 机制的核心铰链。以上机制均可在 CPython 3.11+ 本地直接验证,无需第三方库。通读 asyncio 四章后再接 Web 或爬虫项目,体会最深。机制优先,API 次之。本章至此收工,后续见系列第 27 篇。
小结
async def 定义 协程函数;调用产生 协程,须 Task 或 asyncio.run 调度。await 在 可等待对象 上 暂停 并把 控制权 交还 事件循环,完成后 恢复。普通同步调用不会因 async def 变非 阻塞。下一篇讲工程模式:create_task、gather、cancel、Semaphore。