asyncio.shield 必须用于关键清理操作(如日志落盘、事务提交、连接关闭),防止被取消中断导致数据不一致;应只包裹协程本身(shield(coro)),而非 Task,且内部子 await 需单独 shield。
asyncio.shield 什么时候必须用?
当一个 asyncio.Task 正在执行关键清理、日志落盘、数据库事务提交或网络连接优雅关闭时,如果外部调用 task.cancel() 或超时触发取消,这些操作可能被中断,导致数据不一致或资源泄漏。此时必须用 asyncio.shield() 包裹——它不会阻止取消请求本身,但会让被包裹的协程不响应取消信号,直到它自然完成。
- 关键点:被
shield()包裹的协程仍可被“标记为已取消”,但其内部的await不会抛出CancelledError,除非它自己主动检查asyncio.current_task().cancelled() - 常见误用:对整个
asyncio.create_task()调用加shield(),实际应只包裹内部协程(即shield(some_coro()),不是shield(create_task(some_coro()))) - 注意:
shield()不是“防取消保险箱”,如果被包裹协程里有其他未 shield 的子await(比如调用了第三方异步库),那些子调用仍可能被取消
怎么正确包裹一个清理协程?
典型场景:HTTP 请求处理中,无论主逻辑是否超时,都要确保写入审计日志。
import asyncioasync def write_audit_log(user_id: str):
模拟耗时但不可中断的操作
await asyncio.sleep(0.5) print(f"[AUDIT] logged for {user_id}")async def handle_request(user_id: str):
try:主逻辑带超时
await asyncio.wait_for( do_business_logic(user_id), timeout=0.3 ) finally: # ✅ 正确:shield 只包 cleanup 协程本身 await asyncio.shield(write_audit_log(user_id))async def do_business_logic(user_id: str):
await asyncio.sleep(0.4) # 超时触发
return "done"
- 错误写法:
asyncio.shield(asyncio.create_task(write_audit_log(user_id)))—— 这 shield 的是一个已启动的 Task 对象,没意义 - 正确姿势:直接
await asyncio.shield(write_audit_log(user_id)),让 event loop 知道这个协程需要“免疫”取消 - 如果
write_audit_log内部还有 await,且你希望其中某一步也绝对不中断(比如写磁盘),那得在那一层再套一次shield()
和 asyncio.create_task + cancel() 配合时的坑
当你手动创建任务并 later 取消时,容易忽略 shield 的“延迟生效”特性:
task = asyncio.create_task(handle_request("u123"))
await asyncio.sleep(0.1)
task.cancel() # 此时主逻辑可能刚进 finally 块
await task # ⚠️ 这里会卡住,直到 write_audit_log 完成
- 表现:程序看似 hang 住,其实是
shield()在起作用 ——task.cancel()已生效,但await task会等 shielded 部分跑完才返回 - 解决思路:不要
await task,改用asyncio.wait([task], timeout=...)控制等待上限;或者把 shielded 清理逻辑拆成独立后台任务(用asyncio.create_task()启动,不 await) - 兼容性注意:Python shield() 包裹的协程若在取消后调用
await asyncio.sleep(0),可能意外抛出CancelledError;3.11+ 已修复
替代方案比对:timeout vs shield vs ensure_future
-
asyncio.wait_for(coro, timeout=...):适合控制主流程时限,但不保护后续清理
-
asyncio.shield(coro):专为“无论如何都要做完”设计,但不能设超时,也不能自动重试
-
asyncio.ensure_future(coro):只是把协程转成 Task,完全不提供取消防护,别混淆
- 真实项目中常见组合:
await asyncio.wait_for(asyncio.shield(cleanup()), timeout=2.0) —— 既要求完成,又防无限阻塞
asyncio.wait_for(coro, timeout=...):适合控制主流程时限,但不保护后续清理asyncio.shield(coro):专为“无论如何都要做完”设计,但不能设超时,也不能自动重试asyncio.ensure_future(coro):只是把协程转成 Task,完全不提供取消防护,别混淆await asyncio.wait_for(asyncio.shield(cleanup()), timeout=2.0) —— 既要求完成,又防无限阻塞真正难处理的是嵌套取消:比如父 task 被 cancel,它启动的子 task 又调了 shielded 协程。这时 shield 只保最内层,外层异常传播链仍存在,需靠显式 try/except CancelledError 拦截并重试或降级。这点容易被忽略。
就爱读