跳到主要内容

async / await、异步迭代与异步上下文

本节目标

查询 Python 协程、awaitable、异步迭代器、异步生成器和异步上下文协议。

本章只讨论 Python 的异步语言协议及其清理边界。以协程语句参考await 表达式参考awaitable 对象数据模型异步迭代器数据模型异步上下文管理器数据模型asyncio.run() 为准。前一章的模块入口边界见模块、包与导入系统,对象生命周期的资源可达性问题可接续对象生命周期、垃圾回收与弱引用

协程函数、协程对象与调用边界

不含 yieldasync def 定义协程函数;调用它返回协程对象,函数体尚未执行。对象要到被 await 驱动时才开始运行,因此“调用”既不取得最终结果,也不等同于执行函数体。协程对象应由拥有该调用边界的异步代码等待一次,而不是被当作普通返回值保存后遗忘;含 yieldasync def 则产生异步生成器。

协程对象和函数是不同层次:前者代表一次尚未完成的执行,后者只是可调用定义。入口处的 asyncio.run() 在本页示例中仅负责驱动顶层协程,语言协议本身不依赖特定业务调度方案。

awaitable 协议与 __await__

await 接受协程对象、实现 awaitable 协议的对象等可等待对象。自定义对象的 __await__() 必须返回迭代器;该迭代器完成时的返回值就是 await 表达式的结果。不要只返回一个值或普通可迭代对象来冒充协议,因为驱动端需要的是迭代器合同。

async_protocol_report.py
import asyncio


class Immediate:
def __init__(self, value: int) -> None:
self.value = value

def __await__(self):
if False:
yield None
return self.value


class Countdown:
def __init__(self, start: int) -> None:
self.current = start

def __aiter__(self):
return self

async def __anext__(self) -> int:
if self.current == 0:
raise StopAsyncIteration
value = self.current
self.current -= 1
return value


async def squares(limit: int):
for value in range(limit):
yield value * value


class AsyncResource:
def __init__(self, events: list[str]) -> None:
self.events = events

async def __aenter__(self) -> str:
self.events.append("enter")
return "resource"

async def __aexit__(self, exception_type, exception, traceback) -> bool:
self.events.append("exit")
return False


async def cancellation_probe(cleanup: list[bool]) -> None:
try:
raise asyncio.CancelledError
finally:
cleanup.append(True)


async def main() -> None:
awaited = await Immediate(42)
countdown = [value async for value in Countdown(3)]
generated = [value async for value in squares(3)]
resource_events: list[str] = []
async with AsyncResource(resource_events) as resource:
resource_events.append(f"use:{resource}")
cleanup: list[bool] = []
try:
await cancellation_probe(cleanup)
except asyncio.CancelledError:
pass

print(f"await={awaited}")
print(f"async-for={countdown}")
print(f"async-generator={generated}")
print(f"async-with={','.join(resource_events)}")
print(f"cancel-cleanup={cleanup == [True]}")


asyncio.run(main())
await=42
async-for=[3, 2, 1]
async-generator=[0, 1, 4]
async-with=enter,use:resource,exit
cancel-cleanup=True

示例的 Immediate.__await__() 返回一个能立即结束的生成器迭代器,因此 await Immediate(42) 得到 42;这演示协议形状,而不是把任意同步计算变成异步操作。

await 的暂停、恢复与异常传播

await 把当前协程的控制权交给所等待对象的迭代过程:它可以暂停,随后以值恢复,也可以在恢复处抛出异常。异常没有因跨越 await 自动消失,调用方应在自己拥有恢复策略的层级捕获或继续传播。

取消通常表现为在暂停边界注入异常时,finally 仍会执行。清理代码应在 finally 或合适的上下文退出处维护不变量;它不应假设协程一定沿正常结果路径恢复。

异步迭代器协议

异步迭代对象提供 __aiter__,并由 __anext__ 提供下一项的可等待结果。async for 会等待每次 __anext____anext__StopAsyncIteration 表示完成,而不是 StopIteration。示例中的 Countdown3、2、1 产出,之后以该停止信号结束循环。

协议作者要让停止信号只表达“没有下一项”;其他异常仍是失败路径。调用方使用 async for 时不需要手动捕获这个正常的完成信号。

异步生成器

含有 yieldasync def 产生异步生成器对象,可逐项异步迭代。异步生成器不是普通协程:它不能像普通协程那样直接作为一个最终结果被等待,而是通过异步迭代逐项产生值。squares(3) 的三个值在示例中被异步推导式收集为 [0, 1, 4]

异步生成器还支持向暂停点传值或抛入异常的协议操作;这些能力不改变“产出者负责自身 finally 清理”的所有权。

异步推导式

异步推导式在异步函数中把 async for 的逐项等待与列表、集合或字典构造组合起来。它仍遵循异步迭代器的停止规则,并有自己的推导式作用域;不要把其中的循环变量当作外层局部名称。

示例的 [value async for value in Countdown(3)] 和对 squares(3) 的收集都只是协议消费者。推导式适合表达转换与收集,资源寿命或异常处理需要明确时应改用具名的异步代码块。

异步上下文管理器协议

async with 进入时调用 __aenter__,退出时调用 __aexit____aenter____aexit__ 的结果都会被等待。前者的完成值绑定给 as 目标,后者接收退出时的异常信息;其真值结果可抑制异常,只有管理器确实处理该异常时才应返回真值。

示例记录 enter,use:resource,exit,说明退出钩子在离开块时仍会运行。资源管理器拥有进入后成功取得的资源,应把释放放在退出实现内,避免调用方与管理器重复关闭同一资源。

finally、取消与资源清理

取消在暂停边界注入异常时,finally 仍会执行。代表脚本为了确定性地展示这条异常路径,直接构造 CancelledError 并由外层捕获;它不是完整的并发控制模式。cancellation_probefinally 追加清理标记,输出中的 cancel-cleanup=True 锁定了“异常传播前清理已发生”的边界。

finally 的职责是保持必须的清理或状态恢复,不是无条件吞掉异常。若清理本身失败,应明确考虑它与原异常的关系,不能把失败静默伪装为成功。

异步生成器关闭与清理

未穷尽的异步生成器若不再使用,应由拥有它的一方 await generator.aclose()aclose() 请求生成器关闭,使其在暂停的 yield 点接收关闭信号并执行 finally;这让生成器可以释放自己创建但尚未自然结束的资源。

自然耗尽同样会离开生成器,但不要把垃圾回收时机当成资源关闭协议。需要提前停止时,显式关闭比依赖对象何时不可达更能表达所有权。

协程生命周期、取消与清理

调用协程函数后,若既未 await 该对象,也未把它交给负责驱动的任务或其他拥有者,函数体就不会执行;__await__ 不返回迭代器,对象就不满足 awaitable 合同;__anext__ 使用错误的停止异常,则会破坏异步迭代的完成语义。对于未穷尽的异步生成器,拥有者还必须通过 aclose() 触发清理。

长时间阻塞的同步调用会阻止当前线程继续推进其他异步工作,不应直接放进需要让出控制权的异步路径。同一资源也不应同时由上下文管理器和外层手动关闭。先确认对象实现的协议,再把结果、异常和清理责任放进同一明确的拥有边界。