异步生成器中cancellederror只能在await点被捕获,yield本身不是await点,因此try/except cancellederror无法拦截取消;清理逻辑必须放在async with或finally块中,且finally内的await会执行。

async def 里 yield 遇到 cancel 会直接抛 CancelledError,不进 except
Python 异步生成器(async def + yield)中,如果任务被取消(比如 asyncio.CancelledError 被抛出),它不会像普通协程那样允许你在 try/except 中“捕获后继续 yield”——一旦取消信号到达,生成器会立即终止,yield 后的代码不执行,且 except CancelledError 块**根本不会进入**,除非你显式在 yield 之前或之后加了可中断点。
这是因为 CancelledError 是协作式中断:它只在 await 点(如 await asyncio.sleep()、await anext())被检查并抛出。而 yield 本身不是 await 点,所以 cancel 不会在 yield 那一刻爆发;但当你用 anext() 拉取下一个值时,运行时会先检查是否已取消,再决定是否执行生成器体 —— 这就是为什么你常看到错误报在 await anext(agen) 而不是生成器内部。
- 常见错误现象:
RuntimeError: async generator ignored GeneratorExit或任务静默退出,没走清理逻辑 - 关键事实:
CancelledError在异步生成器中只能在await表达式处被捕获,不能靠包围yield的 try/except 拦截 - 真正能响应取消的位置:生成器函数体内的
await调用点,或生成器退出前的async with/finally块
用 async with + finally 做资源清理,而非依赖 except CancelledError
想确保连接、文件、锁等资源被释放?别指望在 except CancelledError: 里 close,那大概率不执行。正确做法是把清理逻辑放进 async with 上下文管理器,或显式写在 finally 块里 —— 因为 Python 保证:即使异步生成器因取消而退出,只要它已进入 finally(即已执行到该语句位置),就会运行其中的 await 调用。
async def stream_data():
conn = await open_connection()
try:
while True:
data = await fetch_chunk()
yield data
finally:
await conn.aclose() # ✅ 这里会被执行,即使任务被取消
-
finally块中的await是安全的,且会实际执行(前提是生成器已走到该finally) - 避免在
finally里做耗时或可能再次 raise 的操作,否则可能掩盖原始CancelledError - 若需区分“正常结束”和“被取消”,可在
finally前加try/except CancelledError并设标记,但通常没必要 —— 清理逻辑本就该无条件执行
主动检查 asyncio.current_task().cancelled() 配合 await 避免阻塞
如果你的异步生成器里有长周期非 await 循环(比如密集计算或轮询),cancel 信号可能迟迟不生效。这时得手动插入检查点:调用 asyncio.current_task().cancelled(),并在返回 True 时主动 raise CancelledError,让它在可控位置爆发,从而触发 finally 或外层处理。
async def polling_stream():
while True:
if asyncio.current_task().cancelled():
raise asyncio.CancelledError # ✅ 主动抛出,让 finally 触发
result = do_some_cpu_work() # 比如解析大 JSON
if result:
yield result
await asyncio.sleep(0.1) # ⚠️ 这个 await 本身也是检查点
- 仅当存在非 await 的长耗时代码时才需要手动检查;纯 await 链路中,cancel 自然在下一个 await 处生效
- 不要在
yield后立刻检查 —— yield 不挂起,检查无效;要放在可能阻塞的位置之前 -
raise CancelledError和系统自动抛出效果一致,会触发同级finally和外层except
外层消费时用 create_task + cancel 配合 timeout,别直接 await anext
异步生成器的取消行为高度依赖消费者怎么拉取。直接 await anext(agen) 没有超时或取消控制;更健壮的做法是用 asyncio.create_task() 包一层,并在需要时 task.cancel(),或用 asyncio.wait_for()。
agen = stream_data()
task = asyncio.create_task(anext(agen))
try:
result = await asyncio.wait_for(task, timeout=5.0)
except asyncio.TimeoutError:
task.cancel()
await task # 等它完成清理
raise
- 直接
await anext(agen)被取消时,异常抛给调用方,但生成器内部的finally是否执行,取决于它当时执行到了哪 —— 所以必须确保 task 被await完(哪怕为 cancel) -
task.cancel()后记得await task,否则finally可能跳过 - 避免用
async for遍历未受控的异步生成器 —— 它无法优雅中断;改用手动anext+ task 控制
最易被忽略的一点:异步生成器的生命周期由「消费者驱动」,而不是生成器自己。它的 finally 是否执行,取决于你有没有让控制流走到那里 —— 所以 cancel 的可靠性,最终落在你怎么启动、等待、取消那个 anext() task 上。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











