asyncio.wait_for 仅取消任务而不释放锁或清理资源,易致死锁;需在 CancelledError 处理中显式释放资源,并用 shield + timeout 组合确保清理逻辑执行。

asyncio.wait_for 为什么不能只靠 timeout 参数?
因为 asyncio.wait_for 在超时后只是取消任务,但不会自动清理底层资源或释放等待的锁。如果被等待的协程内部持有了 asyncio.Lock、asyncio.Semaphore 或正在 await 一个未响应的网络请求(比如卡在 DNS 解析或 TCP 握手),取消操作可能无法及时生效——协程状态变成 CancelledError 被抛出,但 lock.acquire() 已经阻塞在内核态,释放逻辑根本没机会执行。
常见现象:多次调用后出现「看似超时了,但后续任务依然拿不到锁」,本质是锁没被正确释放。
- 务必在
try/except CancelledError块中显式释放锁或关闭连接 - 避免在
wait_for包裹的协程里做无中断点的 CPU 密集操作(如大循环) - 对 I/O 操作,优先使用支持 cancellation 的库(如
aiohttp默认可取消;aiomysql需确认版本是否修复了 cancel 后连接泄漏问题)
timeout 设多少才合理?别硬写固定值
设成 5 秒看起来安全,但实际可能让下游服务雪崩:假设你并发发起 100 个带 5 秒 timeout 的 HTTP 请求,后端接口平均响应 800ms,但某次网络抖动导致 20 个请求卡在 4.9 秒才返回,它们会同时挤占线程/连接池资源,拖慢其余请求。真正的 timeout 应该是「业务容忍延迟」+「链路冗余」,而不是拍脑袋。
- HTTP 调用建议用「基础延迟 × 2~3 倍 + 固定缓冲(如 200ms)」,例如 p95 延迟是 600ms,就设为
1500 - 数据库查询按慢查询日志调整,别统一用 3 秒——简单
SELECT id FROM user WHERE id = ?和复杂 JOIN + ORDER BY LIMIT 1000 差两个数量级 - 对不可控外部依赖(如第三方 API),必须加 jitter:随机浮动 ±10%~20%,防止重试风暴
捕获异常时,CancelledError 和 TimeoutError 到底该 catch 哪个?
asyncio.wait_for 超时时抛出的是 asyncio.TimeoutError(继承自 Exception),而任务被取消时(比如父 task 调用了 cancel())抛出的是 asyncio.CancelledError(继承自 BaseException)。两者处理方式完全不同:前者是业务超时,可降级或重试;后者是运行时中断,通常不该被 except 捕获,否则会吞掉取消信号,导致死锁。
立即学习“Python免费学习笔记(深入)”;
- 只用
except asyncio.TimeoutError:处理超时逻辑(比如返回默认值、打日志、触发告警) - 绝不要写
except Exception:或except BaseException:来兜底,这会拦截CancelledError - 如果必须在 finally 块里做清理,用
try/finally,不要依赖except——CancelledError仍会向上冒泡
真正防死锁的关键:用 shield + timeout 组合
单独用 wait_for 仍有风险:当任务被取消时,其子任务(比如嵌套的 asyncio.create_task(sub_coro()))可能没被 cancel,继续运行并持有资源。这时候要用 asyncio.shield() 保护关键清理逻辑,确保它不被外层取消打断。
async def guarded_fetch(url):
task = asyncio.create_task(fetch_with_cleanup(url))
try:
return await asyncio.wait_for(task, timeout=2.0)
except asyncio.TimeoutError:
# 这里 task 还在跑,但已超时
task.cancel()
try:
# shield 确保 cleanup 不被再次 cancel
await asyncio.shield(task)
except asyncio.CancelledError:
pass # cleanup 已完成或被中断,忽略
raise
这个模式的核心不是“避免取消”,而是“控制取消时机”——先发 cancel,再 shield 等它收尾,比直接放弃更稳妥。实际项目里,所有涉及锁、连接、文件句柄的异步操作,都应该按这个节奏设计。
最容易被忽略的是:很多协程里用 await asyncio.sleep(0) 当作 yield 点,但这对取消不敏感;真要让取消立即生效,得用 await asyncio.wait([task], return_when=asyncio.FIRST_COMPLETED) 这类可中断原语。


















