← [ BACK_TO_NOTES ]

asyncio 任务、超时与取消

用 Task 管理并发协程生命周期;以 timeout 与 cancel 实现截止与协作式退出,避免孤儿任务与资源泄漏。

#concept #growing #dev #python #python

[!info] related notes

asyncio 任务、超时与取消

这个概念为什么出现

会写 await asyncio.gather(...) 只够演示。线上故障长这样:

  • 某个上游卡住,请求线程/协程被拖死,没有超时
  • 客户端已断开,服务端还在跑完整条 LLM 调用链
  • create_task 火后不管,异常在后台静默,资源不放

asyncio 的并发单元是 Task。正确性不只是“结果算对”,还包括:

  1. 什么时候必须结束(超时/截止)
  2. 如何协作退出(取消)
  3. 谁负责回收异常与清理(await / TaskGroup / finally)

这与 Go 的 context 解决问题同族,但 API 与传播方式不同:Python 更常是“取消 Task + 在 await 点抛 CancelledError”。

[!abstract] 一句话理解 create_task 把协程变成可并发调度的 Task;wait_for/timeout 限制等待;cancel 协作式注入取消;必须有人回收 Task,并在 finally 中释放资源。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:查询上游报价,超过 100ms 放弃并取消后台任务

交易页给外部报价设置 100ms 等待预算。wait_for 到期会请求取消被等待任务,并等待取消处理完成;本例随后返回降级。清理耗时或吞掉取消都可能让实际等待超过 100ms,因此这不是硬性返回时限。

# asyncio_timeout_demo.py
# 业务意图:上游超时则失败,并确保后台报价协程被取消清理。
# 教学点:
# - create_task 并发启动;
# - wait_for 超时;
# - wait_for 已负责请求取消并等待收尾;
# - CancelledError 在 await 点被感知。

from __future__ import annotations

import asyncio


async def slow_quote() -> str:
    # 业务:请求慢上游。用 sleep 模拟 RTT(不是锁)。
    try:
        await asyncio.sleep(0.5)
        return "quote:100"
    except asyncio.CancelledError:
        # 关键:取消路径也要做清理(关连接、还连接池等)。
        print("quote cancelled; cleanup")
        raise


async def fetch_with_budget() -> str:
    task = asyncio.create_task(slow_quote(), name="quote")
    try:
        # 100ms 到期请求取消;取消清理仍可能继续耗时
        return await asyncio.wait_for(task, timeout=0.1)
    except TimeoutError:
        print("timeout")
        return "quote:degraded"


async def main() -> None:
    print("result:", await fetch_with_budget())


if __name__ == "__main__":
    asyncio.run(main())

建议运行:

python asyncio_timeout_demo.py

期望输出:

quote cancelled; cleanup
timeout
result: quote:degraded

结合场景再看四个关注点

  1. 超时是产品语义,不是可选项。
  2. CancelledError 在 await 点出现——协作式,不是杀线程。
  3. 手动 cancel 后应等待任务结束;本例的 wait_for 已完成这一步,无须在 except TimeoutError 中重复取消。
  4. 降级返回与“继续傻等”是两种产品;实现必须匹配。

核心概念与准确模型

1. Task 生命周期

stateDiagram-v2
  [*] --> Pending: create_task
  Pending --> Running: 循环调度
  Running --> Pending: await 等待
  Running --> Cancelling: cancel()
  Cancelling --> Cancelled: 取消异常传播至任务结束
  Cancelling --> Running: 吞掉取消并继续
  Cancelling --> Done: 清理抛出其他异常或返回
  Running --> Done: return
  Running --> Done: 其它异常
  Done --> [*]
  • done() / cancelled() / result() / exception()
  • 从未 await 的 done task,其异常可能只在回调/日志中显现(版本与调试模式有关)——工程上应显式回收

2. 超时 API

API作用
asyncio.wait_for(aw, timeout)到期请求取消并等待;取消传播时转为 TimeoutError,任务吞取消后可能返回结果或抛其他异常
asyncio.timeout(seconds)(3.11+)到期取消当前任务;离开上下文时把自己的取消转为 TimeoutError,通常在上下文外捕获
自建 deadline用循环时间计算剩余预算

自 Python 3.11 起 wait_for 抛内置 TimeoutError,asyncio.TimeoutError 是其别名;本篇示例按 3.11+ 描述。被调用代码自己抛出的 TimeoutError 也会进入同一 except,不能只凭类型证明是预算到期。

3. 取消语义

  1. task.cancel(msg=...) 请求取消
  2. 事件循环获得调度机会时向协程注入 CancelledError,通常表现为挂起的 await 抛出;完成对象的 await 未必让出执行权
  3. 若任务吞掉 CancelledError 且继续运行,可能“取消失败”

清理模板:

try:
    await work()
finally:
    await close_resource()

异步清理本身也可能被再次取消或失败;这个模板不保证在强制退出、重复取消时完成全部清理。

4. 结构化并发(TaskGroup,3.11+)

async with asyncio.TaskGroup() as tg:
    tg.create_task(coro1())
    tg.create_task(coro2())
  • 子任务以非 CancelledError 异常失败时,通常取消同组其他任务并等待;单个子任务被取消不等同于组失败。
  • 离开上下文时等待子任务结束,普通失败汇总为 ExceptionGroup/BaseExceptionGroup;KeyboardInterrupt/SystemExit 有特殊重抛规则。
  • 比“一堆 fire-and-forget create_task”更安全

5. gather vs TaskGroup vs 手动 task

方式优点风险
gather按输入顺序返回结果默认首个异常立即传播,不自动取消其他任务;return_exceptions=True 把异常当结果
TaskGroup结构化版本要求 3.11+
手动 task灵活易漏回收

6. shield(高级,慎用)

asyncio.shield(aw) 保护 awaitable 不被外层取消直接取消。
超时场景误用 shield 可能导致“外层超时了,内层还在跑”。本篇主示例有意不使用 shield。

设计动机

事件循环不能强行杀掉任意 Python 帧(可能持有锁/不变量)。因此取消是协作式:

  • 在明确的 await 边界插入取消
  • 让代码用 try/finally 维护资源安全

超时则是把“用户/下游等待预算”编码进控制流,与 Go context.WithTimeout 目标相近。

边界情况与反直觉行为

1. 取消点不在 CPU 空转

纯计算循环若无 await,会延迟响应取消。需要在循环中 await asyncio.sleep(0) 或把计算丢线程/进程。

2. wait_for 与已完成 task

已完成则直接返回结果;超时逻辑不会倒放。

3. 超时后 task 状态

默认 wait_for 抛出其超时异常时,通常已等到被取消任务结束;若任务吞取消,可能返回正常结果。shield 保护的内层可能仍运行,必须有 owner 后续回收。

4. CancelledError 继承关系

在 3.8+ 起 CancelledError 是 BaseException 的子类(不是普通 Exception)。
except Exception: 抓不住它——这是刻意设计,避免误吞取消。

5. 任务里再创建子任务

取消父任务不会自动完美取消你手动挂起的所有子图,除非你用 TaskGroup 或显式级联 cancel。

常见误区

[!warning] 常见误区:超时后不管 task 错误理解:函数返回即可。
正确模型:明确谁负责取消和等待;默认 wait_for 已负责,shield 或仅停止等待的设计则需独立 owner 收尾。

[!warning] 常见误区:except Exception 包住整段协程 错误理解:统一错误处理。
正确模型:Python 3.8+ 的 except Exception 不会捕获 asyncio.CancelledError;若显式捕获取消,清理后通常应重抛。

[!warning] 常见误区:fire-and-forget create_task 错误理解:后台跑就行。
正确模型:保存引用、回收异常、定义生命周期所有者。

[!warning] 常见误区:用 sleep 等待“应该取消了” 错误理解:sleep 0.1 再继续。
正确模型:await 被取消的 task 本身。

与相邻概念对比

机制领域传播方式
asyncio 取消Python 协程任务Task.cancel → await 点
thread Event线程协作查 flag
process terminate进程强杀/信号
Go context调用树ctx.Done 传播

文字说明:异步服务关闭、请求取消、上游超时,应统一成“预算与取消图”,而不是各写各的 sleep。

工程实践

请求级预算

  • 入口设置总超时
  • 下游调用使用剩余预算
  • 超时指标:timeout_total / 按依赖名

资源清理清单

  • HTTP session / DB 连接
  • 文件句柄
  • 队列 consumer 的 task_done
  • 后台心跳 task

与 Web 框架

  • 客户端断开时取消 handler task(框架相关)
  • 阻塞库调用放 to_thread,并仍要能响应取消(线程中断有限——设计时避免长阻塞)

推荐默认

  1. 外部 I/O 必有 timeout
  2. 后台 task 有 owner
  3. 优先 TaskGroup(版本允许)
  4. 不随意 shield

可验证实验

实验 1:主路径

运行示例,确认出现 timeout 与 quote cancelled; cleanup。

实验 2:放宽超时

把 wait_for(..., 0.1) 改为 1.0,应打印 result: quote:100 且无 cancelled。

实验 3:吞掉 CancelledError(反例)

在 slow_quote 的 except CancelledError 里直接 return “nope”,wait_for 会得到这个结果而非超时降级;可见请求取消不等于任务最终处于 cancelled。实验后恢复重抛。

实验 4:TaskGroup(3.11+)

用 TaskGroup 同时拉两个慢调用,让其中一个失败,观察另一个是否被取消。

实验 5:对比漏 await

用 wait_for(shield(task), 0.1) 只限制外层等待,捕获超时后检查 task.done(),再显式 cancel 并 await 收尾。不要拿默认 wait_for 当作“漏取消”反例。

本节总结

  • Task 是生命周期对象,不是“随便丢后台”的句柄。
  • 超时与取消是异步正确性的一等公民。
  • 协作式清理靠 await 点 + finally;except Exception 不能当万能网。

自测题

概念题

  1. 为什么 cancel 后还要 await task?
  2. 为什么 except Exception 不应包住取消?

代码推理题

  1. wait_for(sleep(1), 0.1) 超时后,sleep 任务通常处于什么状态?

工程思考题

  1. 网关已有 3s 超时,服务内是否还要设下游超时?为何?
参考答案
  1. 确认任务结束并跑完清理,避免孤儿任务与未回收异常。
  2. Python 3.8+ 中它不是 Exception 子类,该 except 不会吞取消;显式捕获后无意吞掉才会破坏传播。
  3. 普通 sleep 任务在 wait_for 抛 TimeoutError 前已完成取消;若任务包含异步清理,等待可能超过预算。
  4. 要;网关超时后服务可能仍在工作。分层预算避免无效计算与资源占用。

延伸阅读与资料来源

资料类型支撑内容
Tasks and coroutines文档Task/cancel/wait_for/TaskGroup
Timeouts文档超时 API
PEP 3156PEP异步 IO 背景
Trio nursery 思想文档结构化并发对照(概念)

笔记元信息

  • 角色:原子概念(asyncio 正确性关键)
  • 前置必读:python-async-programming
  • 对照:Go context 传播 vs Task 取消
创建于 2026/7/15 更新于 2026/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本