← 返回题目列表

asyncio.shield 有什么用?它能阻止任务被取消吗?

高频 困难 第 14 / 27 题 更新于 2026/07/31
asyncioshield取消CancelledError

简化版

asyncio.shield() 可以保护内部 awaitable 不因为外层等待者被取消而立刻取消,但它不能让任务“绝对不可取消”。如果内部任务自己被取消,或事件循环关闭,它仍会结束;外层协程也仍会收到 CancelledError

详细版

取消是 asyncio 里的重要控制流。普通 await task 时,外层协程被取消,取消通常会传播到正在等待的 task。asyncio.shield(task) 的作用是隔离这条传播:外层等待被取消时,内部 task 可以继续运行。

task = asyncio.create_task(save_data())
try:
    await asyncio.shield(task)
except asyncio.CancelledError:
    # 外层被取消,但 task 可能还在继续
    raise

它适合保护短小、关键、最好完成的收尾动作,比如提交审计日志、释放远程锁、写入最终状态。但不能滥用,否则会让服务关闭变慢、后台任务失控。

完整版教学

一、为什么取消传播需要被控制

异步程序中取消很常见:请求断开、超时、服务关闭、任务组失败,都会取消正在运行的协程。取消默认会沿着 await 链向下传播,这通常是好事,因为它能快速释放资源。

handler 被取消
  |
await save_data()
  |
save_data 也收到取消

但有些动作不希望中途被外层取消打断。比如已经写了一半的最终状态,或者拿到分布式锁后必须释放。shield 就是为了控制这类取消传播。

二、shield 保护的到底是谁

shield(awaitable) 保护的是内部 awaitable 不受“等待它的外层任务取消”影响。外层任务被取消时,外层 await shield(...) 仍会抛 CancelledError,但内部 task 不会因此自动取消。

outer task cancel
  |
await shield(inner task)
  |
outer 收到 CancelledError
inner 继续运行

这个细节很关键:shield 不是吞掉取消,也不是让外层继续正常执行。它只是让内部任务和外层取消解耦。

三、一个数字化例子

假设请求处理总超时是 100ms,业务已经处理完,最后需要 20ms 写审计日志。如果在第 90ms 外层收到取消,普通 await 可能让审计写入中断;shield 后外层请求仍结束,但审计 task 可以继续跑完。

async def handler():
    audit_task = asyncio.create_task(write_audit())
    try:
        await asyncio.shield(audit_task)
    except asyncio.CancelledError:
        # 请求取消了,但审计任务仍可能完成
        raise
0ms 业务完成
80ms 开始审计
90ms 外层取消
100ms 外层返回取消
105ms 审计完成

shield 适合“短小关键尾动作”,不适合保护无限长的后台任务。

四、shield 不能阻止哪些取消

如果内部 task 自己被取消,shield 没法救它。如果内部代码主动抛 CancelledError,或者别人拿到 task 调用了 task.cancel(),shield 外面也会看到取消。事件循环关闭时,未完成任务也可能被取消。

task = asyncio.create_task(work())
task.cancel()
await asyncio.shield(task)  # 仍会收到 CancelledError

所以 shield 的边界是“阻断外层等待者取消向内传播”,不是给任务套无敌护盾。命名有点容易误导,面试时要把这个边界说清。

五、和 wait_for / timeout 的关系

asyncio.wait_for(coro, timeout=1) 超时时默认会取消内部任务。如果你把内部任务 shield 起来,超时会让外层等待结束,但内部任务可以继续。

task = asyncio.create_task(slow_io())
try:
    await asyncio.wait_for(asyncio.shield(task), timeout=1)
except TimeoutError:
    print("outer timeout, task may still run")

这在某些场景有用,比如前端不等结果了,但后台保存还要继续。代价是任务生命周期必须有人管理,否则它变成“孤儿任务”,异常没人收集。

六、工程上怎么安全使用

使用 shield 时要保存 task 引用,并决定后续如何观察它的结果。可以在 finally 中检查任务状态,或把任务交给统一后台任务管理器。不要创建一个 shield 后完全不管。

task = asyncio.create_task(flush_metrics())
try:
    await asyncio.shield(task)
finally:
    if task.done() and task.exception():
        logger.error("flush failed", exc_info=task.exception())

长任务更适合任务队列或显式后台管理,而不是靠 shield 偷偷跑。服务关闭时也要给这些任务一个最大等待时间,避免优雅关闭无限拖延。

七、常见误区与追问

  • 误区:shield 能让任务永远不被取消。 不能;内部任务被直接取消时仍会取消。
  • 误区:shield 会吞掉外层 CancelledError。 不会,外层等待者仍会收到取消。
  • 误区:所有清理动作都应该 shield。 只有短小关键动作适合,长耗时任务要有专门生命周期管理。
  • 追问:wait_for 超时后 shield 里的任务会怎样? 外层超时结束,内部 task 可能继续运行。
  • 追问:shield 后任务异常怎么办? 要保存 task 引用并收集异常,否则可能无人处理。
  • 追问:释放锁需要 shield 吗? 可考虑,但更重要是 finally 中释放,并确保释放动作短且可控。
  • 追问:TaskGroup 里能随便 shield 吗? 要谨慎,结构化并发强调任务共同生命周期,shield 可能破坏这种语义。

八、加强记忆

shield 记成“隔离外层取消传播”,不是“取消免疫”。外层被取消时照样收到 CancelledError,内部 task 只是不会因为这次外层取消而自动停下。使用它时必须保存任务、收集异常、限制时长,否则你只是把问题从当前请求挪成了无人管理的后台任务。