Python asyncio.shield:内层任务受到保护,为什么外层 await 仍然被取消
一个调用方在等待后台整理任务时被取消,希望后台任务完成手头的小段工作,于是把await换成asyncio.shield。结果调用方仍抛出CancelledError,很容易误以为保护没有生效。此时应分别检查两个任务:等待者结束,与被等待者收到取消请求,是不同的状态变化。
下面的程序在Linux、CPython 3.12.14实跑。保存为demo.py,运行python3 demo.py,只创建短暂内存任务,不连接服务或修改文件。两只Event明确控制任务已经开始和等待者已经进入;另一只Event让工作停在固定位置,因此不需要猜测应当sleep多久。
AI模型生成概念图:外部连线止于防护拱,轨道内的小车仍可继续前进,用于比喻取消传播的边界;图片不是异步调试器截图。
先让普通等待成为对照
scenario每次创建一项inner工作和一项outer等待。两轮唯一的关键差别,是outer直接await inner,还是等待shield(inner)。主流程在两者到位后取消outer,再观察inner状态。work的finally记录cleanup,正常完成另记finished,这样取消与完成不会只剩一个模糊的“结束”标记。
完整可运行程序
import asyncio
async def scenario(shielded):
started = asyncio.Event()
entered = asyncio.Event()
release = asyncio.Event()
events = []
async def work():
started.set()
try:
await release.wait()
events.append("finished")
return 23
finally:
events.append("cleanup")
inner = asyncio.create_task(work())
async def caller():
entered.set()
if shielded:
return await asyncio.shield(inner)
return await inner
outer = asyncio.create_task(caller())
await started.wait()
await entered.wait()
outer.cancel()
try:
await outer
except asyncio.CancelledError:
pass
else:
raise AssertionError("caller must receive cancellation")
label = "shield" if shielded else "plain"
print(label, "outer cancelled:", outer.cancelled())
print(label, "inner cancelled:", inner.cancelled())
if shielded:
assert not inner.done() and events == []
release.set()
assert await inner == 23
assert events == ["finished", "cleanup"]
else:
assert inner.cancelled() and events == ["cleanup"]
print(label, "events:", events)
async def main():
await scenario(False)
await scenario(True)
inner = asyncio.create_task(asyncio.sleep(0))
wrapper = asyncio.shield(inner)
inner.cancel()
try:
await wrapper
except asyncio.CancelledError:
print("direct inner cancel:", inner.cancelled())
else:
raise AssertionError("shield must not undo direct cancellation")
asyncio.run(main())本次实际输出(以下为结果,不是程序)
plain outer cancelled: True plain inner cancelled: True plain events: ['cleanup'] shield outer cancelled: True shield inner cancelled: False shield events: ['finished', 'cleanup'] direct inner cancel: True
两组输出分别说明了什么
plain一组的outer和inner都显示cancelled为True,事件只有cleanup。取消等待者沿直接await关系传到inner,inner没有跨过release,所以没有finished。finally仍被执行,证明取消路径的收尾确实发生。
shield一组的outer仍是True,inner却为False。程序还断言inner尚未完成、事件列表为空,随后才打开release,并重新await保存下来的inner。最终finished与cleanup都出现,返回值23也通过检查。这组观察才证明取消传播被挡在了包装层,而不是异常被打印逻辑漏掉。
保护内层不代表替外层忽略取消
outer里的await仍以取消结束,因此主流程必须接收CancelledError。示例在观察层捕获它,目的只是继续比较两组实验;真实调用链不应因加了shield就把取消转换成正常业务成功。若请求已经结束,而后台仍继续处理,两边应该分别记录状态。
如果只在outer外层加宽泛的except,然后丢掉inner引用,表面上没有报错,后台的结果和失败却可能无人收取。示例始终保留inner,并在受保护场景中等待它结束。选择shield之前,先确定哪个对象拥有这项继续运行的工作,以及由谁接收其最终结果。
直接取消内层仍然有效
最后一个小实验先创建inner和shield包装,再对inner本身调用cancel。等待wrapper仍收到CancelledError,输出direct inner cancel为True。shield不是给任务设置不可取消标志;其他持有任务引用的代码仍可以向它发出取消请求。
这个反例也解释了为什么不能仅搜索调用点是否出现shield,就断言任务绝不会中止。排查时要沿任务对象查找其他cancel调用,以及拥有它的任务组或关闭流程。保护某条等待关系,不会改变所有其他管理者的行为。
把继续运行的范围设计得足够小
本文的受保护工作只等一个受控事件,再修改内存列表,主流程明确完成全部收尾。业务中若用shield保护无限循环,或保护没有结束条件的远程等待,就可能把取消问题变成生命周期问题。应先定义可完成的小段操作和后续故障处理,再决定是否需要隔离取消。
图中的防护拱也不表示事务或持久化保证。inner即使最后完成,也不会替调用方撤销此前动作;进程退出、资源关闭等情形还需要另外验证。验收至少要像本例一样同时检查外层取消、内层状态、完成结果以及直接取消反例,不能只以“没看到报错”判断生效。
参考资料
资料核验日期:2026年10月2日。以上输出来自文中固定输入的本地实跑,程序退出码为0。


