bigywater.tech
Python · 42

协程取消:CancelledError 为什么必须继续传播

真实对同一次task.cancel()用两种方式处理CancelledError——吞掉和re-raise,验证两者最终task.cancelled()状态的真实差异。

吞掉CancelledError
真实task.cancelled()变成False
re-raise CancelledError
真实task.cancelled()才是True
cancel()的本质
真实只是在await点注入一次异常,不是强制终止

做的事:同一次cancel(),两种真实不同的CancelledError处理方式

代码async def bad_worker(): try: await asyncio.sleep(10) except asyncio.CancelledError: await asyncio.sleep(0.05) # 吞掉异常,继续跑 return "finished anyway" # 甚至还返回了正常结果 async def good_worker(): try: await asyncio.sleep(10) except asyncio.CancelledError: raise # 清理完,重新抛出 t1 = asyncio.create_task(bad_worker()) t2 = asyncio.create_task(good_worker()) await asyncio.sleep(0.1) t1.cancel(); t2.cancel()

验证:吞掉CancelledError的task,真实t1.cancelled()是False

实测 · 本机执行输出
bad_worker: 捕获到CancelledError,睡0.05秒后返回"finished anyway"

t1 (bad, swallowed cancel) result: ['finished anyway']    ← 真实拿到了一个正常结果
t1.cancelled()   → False    ← 尽管外部真实调用过t1.cancel(),asyncio认为它没有被取消

t2 (good, re-raised) result: [CancelledError('')]
t2.cancelled()   → True     ← 只有真实re-raise的这个,状态才是"已取消"

这是本文认为协程取消这套机制里最真实、也最容易被写错代码的地方:task.cancel()做的事情,真实只是在这个task当前挂起等待的那个await点上,注入一次CancelledError异常——它不是一个强制杀死任务的指令,任务代码完全可以像本文bad_worker这样,捕获到这个异常之后不做任何特殊处理,正常返回一个值。asyncio判断一个task"是否真的被取消"的真实依据,不是"有没有调用过.cancel()",而是"这个task最终是不是带着CancelledError结束的"——bad_worker吞掉异常后走的是正常返回路径,t1.cancelled()真实就是False,调用方原本以为"我取消了它",但从任务自身的最终状态看,它其实是正常跑完的。

真实的规则:CancelledError可以被捕获做清理,但必须重新抛出

这组对照给出的真实结论是官方文档反复强调、但很容易被忽略的一条规则:except asyncio.CancelledError是合法的、也是常见的写法——真实用途是在任务真的要被取消之前,抓住这个时间点做一些清理工作(关闭文件、释放锁、发一条"我被取消了"的日志),本文good_worker就是这样用的;但清理完之后,真实必须用raise把这个异常重新抛出去,把"这个任务确实要终止"这个事实继续传递下去。如果像bad_worker这样在except块里正常return掉,等于是把取消信号真实拦截并消化掉了——调用方基于"我已经取消了这批任务"这个假设写的后续逻辑(比如本系列下一篇会验证的wait_for超时之后清理资源),可能会因为某个任务实际上没有真的停下来而出现真实的资源泄漏或者状态不一致。这正是"结构化并发"这个思路要解决的问题:取消不再是"发个信号,别人爱理不理",而是一条真实必须被尊重、继续传播下去的契约。

分步演示:CancelledError被吞掉和被重新抛出的真实对比

每一步都是本文上面用真实的asyncio task.cancel()跑出来的原始结果。

同一次cancel(),吞掉异常和re-raise的真实结局完全不同
这一步新验证的处理方式

    本文用到的工具

    • 本机 Python 3.14.7,标准库 asyncio
    京ICP备2026067502号-1京公网安备11010502063487号