asyncio.timeout() 与 asyncio.wait_for() 在 3.11+ 的行为变化

asyncio.timeout() 不是 asyncio.wait_for() 的替代品:前者仅提供超时检查且不取消协程,后者显式调度任务并抛 CancelledError;3.11+ wait_for() 默认 strict=True,对已完成协程调用 cancel 会报 RuntimeError。

asyncio.timeout() 不再是上下文管理器的替代品

Python 3.11 引入了 asyncio.timeout(),但它**不是** asyncio.wait_for() 的语法糖或轻量替代。它只提供超时上下文管理行为,内部不封装任务调度逻辑;而 wait_for() 会显式创建任务、处理取消、转发异常——两者职责完全不同。

常见错误是写成这样:

async with asyncio.timeout(1):
    await some_coro()  # ❌ 如果 some_coro() 内部阻塞或未响应 cancel,timeout 不会强制中断它

原因在于 timeout() 只在进入/退出时检查时间,并依赖协程主动 await 可取消点(如 asyncio.sleep()await reader.read())才能响应取消。它不干预协程执行流。

asyncio.wait_for() 在 3.11+ 默认启用 strict=True

从 3.11 开始,asyncio.wait_for()strict 参数默认为 True。这意味着:如果被等待的协程已结束(无论成功或失败),再调用 cancel() 会抛出 RuntimeError: Cannot cancel task when it is in a finished state

典型触发场景:

  • 协程执行极快,在 wait_for() 来得及注册取消前就完成了
  • 超时时间设得过大,但目标协程本身瞬间返回
  • 协程内抛出未捕获异常,导致任务提前终止

解决办法是显式传 strict=False

await asyncio.wait_for(some_coro(), timeout=5, strict=False)

否则你会在日志里反复看到那个 RuntimeError,尤其在压测或高并发 I/O 场景下。

timeout() 和 wait_for() 对 CancelledError 的处理差异

当超时发生时:

  • wait_for() 总是向被包装协程抛出 CancelledError,并等待其完成清理(除非它忽略取消)
  • timeout() 仅在 __aexit__ 阶段检查是否超时,若超时则抛出 TimeoutError,**不会主动 cancel 任何协程** —— 它甚至不知道你在 await 什么

也就是说,如果你写:

async with asyncio.timeout(0.1):
    await asyncio.sleep(1)  # ✅ 会被 sleep 中断,因为 sleep 是可取消的
async with asyncio.timeout(0.1):
    await slow_cpu_bound_func()  # ❌ 不会中断,只是最后抛 TimeoutError,slow_cpu_bound_func 继续跑

这种差异决定了:想真正中断长时间运行的协程,必须用 wait_for();只想给一段代码块加“截止时间提示”,用 timeout() 更轻量。

实际选型建议:看你要中断还是仅标记

wait_for() 当:

  • 需要确保协程在超时后停止执行(比如防止连接泄漏、避免资源占用)
  • 要统一捕获 TimeoutErrorCancelledError
  • 目标协程明确支持取消(即内部有 await 点)

timeout() 当:

  • 只是给一段逻辑加“最晚截止时间”,不关心它是否真停了
  • 嵌套多个异步操作,且不想层层透传 timeout 参数
  • 配合 asyncio.create_task() 自行管理生命周期(此时你控制 cancel 时机)

最容易被忽略的一点:3.11+ 的 wait_for()strict=True 下对“已完成协程”的容忍度极低,而很多旧代码假设它总是静默忽略重复 cancel —— 这个行为变化会在升级后突然暴露出来。