4. 任务取消与异常处理

任务说明

本案例演示 asyncio 任务取消机制:创建两个协程任务,其中一个在运行中被取消,另一个正常完成。通过捕获 asyncio.CancelledError 观察取消行为,并验证任务状态变化。

核心实现

import asyncio

async def worker(name: str, duration: float) -> str:
    """模拟耗时任务,支持取消清理"""
    try:
        print(f"[{name}] 开始执行,计划运行 {duration} 秒")
        await asyncio.sleep(duration)
        print(f"[{name}] 正常完成")
        return f"{name}-done"
    except asyncio.CancelledError:
        print(f"[{name}] 收到取消信号,执行清理...")
        # 清理操作(如关闭连接、释放资源)
        await asyncio.sleep(0.1)  # 模拟清理耗时
        print(f"[{name}] 清理完成,重新抛出取消异常")
        raise  # 必须重新抛出,否则任务会被标记为完成而非取消

async def main():
    # 创建两个任务
    task_a = asyncio.create_task(worker("任务A", duration=3.0))
    task_b = asyncio.create_task(worker("任务B", duration=1.0))

    # 等待0.5秒后取消任务A
    await asyncio.sleep(0.5)
    task_a.cancel()
    print("已发送取消请求给任务A")

    # 收集结果
    results = await asyncio.gather(task_a, task_b, return_exceptions=True)

    # 输出状态
    print(f"\n任务A cancelled 状态: {task_a.cancelled()}")
    print(f"任务B done 状态: {task_b.done()}")
    print(f"任务B 返回值: {task_b.result()}")

    # 检查取消异常
    for i, res in enumerate(results):
        if isinstance(res, asyncio.CancelledError):
            print(f"任务{'A' if i == 0 else 'B'} 被取消,捕获到 CancelledError")
        elif res is not None:
            print(f"任务{'A' if i == 0 else 'B'} 结果: {res}")

asyncio.run(main())

执行流程与取消机制

程序启动后创建两个任务:任务A计划运行3秒,任务B计划运行1秒。主流程在0.5秒时调用 task_a.cancel() 发送取消请求。此时任务A正阻塞在 asyncio.sleep(3) 处,取消请求会向该协程内部抛出 CancelledError。

任务A的 except asyncio.CancelledError 分支捕获异常后先执行清理逻辑(模拟0.1秒),然后通过 raise 重新抛出异常——这是关键点:如果不重新抛出,任务会被标记为“已完成”而非“已取消”,导致 task.cancelled() 返回 False。任务B不受影响,正常完成并返回字符串 "任务B-done"。

验证方法

直接运行上述代码,观察控制台输出顺序:

  1. 两个任务先后打印“开始执行”日志;
  2. 0.5秒后打印“已发送取消请求给任务A”;
  3. 任务A打印“收到取消信号,执行清理...”和“清理完成,重新抛出取消异常”;
  4. 任务B在1秒时打印“正常完成”;
  5. 最终输出状态检查结果。

判定标准:若输出中 任务A cancelled 状态: True、任务B done 状态: True,且任务A的清理日志出现在取消请求之后、状态检查之前,则说明取消机制工作正常。若任务A的 cancelled() 返回 False,说明 except 块中缺少 raise 重新抛出语句。

任务取消时序图 主流程 任务A 任务B create sleep(3) 开始 sleep(1) 开始 cancel 0.5s 捕获 CancelledError 清理 (0.1s) raise 重新抛出 正常完成 返回 "任务B-done" 检查 取消/主流程 任务A 任务B 取消处理

关键要点

  • 取消传播:cancel() 会在任务当前挂起点(如 sleep)注入 CancelledError,若任务正在执行纯CPU计算(无挂起点),取消需等待下一次 await 才能生效。
  • 必须重新抛出:捕获 CancelledError 后若吞掉异常(不 raise),任务会被视为正常完成,cancelled() 返回 False,且外层 gather 无法感知取消意图。
  • 清理时机:在 except 块中执行资源释放操作,但应避免在清理中再次 await 长时间操作,否则会延迟取消完成。