异步生成器中yield后遇cancel不会立即抛CancelledError,而是在下次__anext__()调用时才触发;需在yield后主动检查cancelled()并break,否则资源可能泄漏、finally不执行。

async def 里 yield 后遇到 cancel 会怎样
Python 异步生成器(async def + yield)在被取消时,不会像普通协程那样立即抛出 CancelledError。它会先尝试完成当前 yield 表达式,然后在下一次调用 __anext__() 时才真正抛出异常——但前提是生成器还没退出。这意味着你不能只靠 try/except 包住 yield 就完事。
常见错误现象:CancelledError 被吞掉、协程卡死、资源未清理、finally 块没执行。
- 异步生成器的生命周期由调用方控制(比如
async for),不是靠自身返回或异常中断 -
yield点本身不触发取消检查;真正的取消点是__anext__的入口和出口 - 如果
yield后紧跟耗时await,而此时任务被取消,该await会抛出CancelledError,但若没捕获,会直接冒泡中断生成器,跳过后续清理逻辑
必须在 yield 后手动检查 asyncio.current_task().cancelled()
仅靠 try/except CancelledError 不够稳健,因为取消可能发生在 yield 和下一行之间,且 Python 3.11+ 对异步生成器取消语义做了收紧:未响应取消的任务会被静默终止。主动轮询是最可控的方式。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 在每次
yield后插入if asyncio.current_task().cancelled(): break - 不要依赖
asyncio.shield()来“保护”整个生成器——它会阻碍取消传播,导致调用方永远等不到结束 - 若生成器内部有长期
await(如await asyncio.sleep(30)),应在 await 前后都检查取消状态
示例片段:
import asyncio
<p>async def ticker():
i = 0
while True:
yield i
i += 1</p><h1>主动检查,避免 yield 后挂起太久</h1><pre class="brush:php;toolbar:false;"> if asyncio.current_task().cancelled():
break
await asyncio.sleep(1)使用 asyncgen_hooks 或 __aiter__ 层做统一取消拦截
Python 3.12+ 提供了 sys.set_asyncgen_hooks(),可注册 firstiter 和 finalizer 回调,其中 finalizer 在异步生成器被垃圾回收或显式关闭时调用——但它**不保证在取消时触发**,所以不能替代主动检查。
更可靠的做法是在自定义迭代器封装层拦截:
- 不直接
async for x in my_async_gen(),而是包装成一个类,实现__aiter__并在__anext__中捕获CancelledError - 在
__anext__的try/except中清理资源、关闭连接、发信号 - 注意:必须重新抛出
CancelledError(或调用raise),否则取消信号会丢失
关键点:异步生成器本身没有 aclose() 的自动调用保障,取消 ≠ 关闭,必须自己补上。
测试 cancelled 场景不能只用 asyncio.create_task().cancel()
单纯调用 task.cancel() 并不等于触发异步生成器的取消路径——它只是标记任务为已取消,实际抛错时机取决于事件循环何时调度该任务并进入下一个 await 点。
真实测试要模拟调用方行为:
- 用
async for循环几轮后,从外部task.cancel(),再await task等待完成 - 用
asyncio.wait_for(gen.__anext__(), timeout=0.1)触发超时取消 - 在
yield后加await asyncio.sleep(0)让出控制权,确保取消能及时生效
容易忽略的是:单元测试中若没 await 到生成器结束,CancelledError 可能被压制,表现为“测试通过但线上崩溃”。


















