[RL]协程asyncio.CancelledError

好的,我们来精确地分析在您提供的 long_running_task 代码中,except asyncio.CancelledError 块被执行的精确条件。

python 复制代码
async def long_running_task():
    print("任务开始,进入 try 块...")
    try:
        # ... 代码 A ...
        await asyncio.sleep(1) # <--- 这是一个 await 点
        # ... 代码 B ...
    except asyncio.CancelledError:
        # ... 清理代码 ...

except asyncio.CancelledError: 这个块会被执行,需要同时满足以下两个条件:

  1. 任务必须已经被标记为"取消" : 在 long_running_task 外部的某个地方,必须已经调用了 task.cancel()。这就像"解雇通知"已经被递送到了员工的桌上。

  2. 控制流必须位于 try 块内部,并且到达一个 await 表达式 : 当 Python 的 await 机制准备暂停当前协程时,asyncio 事件循环会检查该任务是否被标记为"取消"。

    • 如果"是" : 事件循环不会 去执行 await 后面的表达式(比如 asyncio.sleep(1))。相反,它会立即在 await 表达式这个位置注入并抛出一个 asyncio.CancelledError 异常。
    • 如果"否" : 正常执行 await 后面的表达式,暂停协程并等待结果。

精确的执行路径分析

让我们用您提供的代码来走一遍精确的路径:

python 复制代码
async def long_running_task():
    print("任务开始,进入 try 块...")
    try:
        print("步骤 1: 开始一项耗时操作...")
        # (1) 这里没有 await,任务不会检查取消状态
        import time
        time.sleep(2) 
        print("步骤 2: 同步操作完成。")

        # (2) 到达第一个 await 点!
        # 在这里,事件循环会检查 task.is_cancelled()
        await asyncio.sleep(1) 

        # (3) 如果没有被取消,会执行到这里
        print("步骤 3: 任务未被取消,继续执行...")
        await asyncio.sleep(1)
        print("步骤 4: 任务正常完成。")

    except asyncio.CancelledError:
        # (4) 异常处理块
        print("步骤 E1: 捕获到 CancelledError!开始清理工作...")
        # ...

在 main 函数中,我们在 task 启动后很快就调用了 task.cancel()。

  1. long_running_task 开始执行,打印"任务开始..."、"步骤 1..."。
  2. 执行 time.sleep(2)。在这 2 秒内,main 函数已经调用了 task.cancel()。现在,task 对象内部的"取消"标志位已经被设置为 True。但是,long_running_task 对此一无所知 ,因为它正被一个同步的 sleep 阻塞,没有机会与事件循环交互。
  3. time.sleep(2) 结束,打印"步骤 2..."。
  4. 代码执行到 await asyncio.sleep(1)。这是关键点!
    • await 表达式将控制权交还给事件循环。
    • 事件循环在准备执行 asyncio.sleep(1) 之前,会检查这个 task 的状态。
    • 它发现:"哦,这个任务的取消标志位是 True!"
    • 于是,事件循环决定不执行 asyncio.sleep(1) 。它立即在这个 await 的位置抛出一个 asyncio.CancelledError。
  5. 这个 CancelledError 异常被 try...except 块捕获。
  6. 程序的控制流立即跳转 到 except asyncio.CancelledError: 块,开始执行 print("步骤 E1: ...")。

因此,except 块被执行的精确时刻是:当一个已经被 cancel() 的任务,在 try 块内执行到它的第一个 await 暂停点时。


如果 try 块内没有 await 会怎样?

这是一个非常好的思考题。

python 复制代码
async def no_await_in_try():
    try:
        print("进入 try 块,但这里没有 await。")
        time.sleep(3)
        print("try 块结束。")
    except asyncio.CancelledError:
        print("永远不会执行到这里!")
    
    # 任务在 try 块结束后,在这里的 await 点才抛出异常
    await asyncio.sleep(0) 
    print("也永远不会执行到这里。")

async def main_test_no_await():
    task = asyncio.create_task(no_await_in_try())
    await asyncio.sleep(0.1)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("Main 捕获到了 CancelledError。")

运行 main_test_no_await 的输出会是:

复制代码
进入 try 块,但这里没有 await。
(等待 3 秒)
try 块结束。
Main 捕获到了 CancelledError。

分析:

  • task.cancel() 被调用时,no_await_in_try 正在 time.sleep(3)。
  • sleep 结束后,try...except 块正常退出,except 块完全没有被执行 ,因为在 try 块的作用域内,没有任何 await 点来触发 CancelledError 的抛出。
  • 当 no_await_in_try 执行到 try 块之后的 await asyncio.sleep(0) 时,它终于有机会检查取消状态,并在这里抛出了 CancelledError。
  • 这个异常因为没有在 no_await_in_try 内部被捕获,所以被传播了出去,最终被 main_test_no_await 中的 await task 捕获。

结论 : try...except CancelledError: 只能捕获到在 try 块内部的 await 点 所触发的 CancelledError。这是理解 asyncio 异常处理和任务取消的关键。

相关推荐
宿州派大星3 小时前
Python实战:PyQt5 + OpenCV + Dlib 打造AI桌面应用
人工智能·python·qt
承渊政道4 小时前
【从零开始大模型开发与微调:基于PyTorch与ChatGLM】(开源大模型ChatGLM高级定制化应用实战)
人工智能·pytorch·python·llm·模型微调·glm
Joy T5 小时前
Agent 确定性上下文设计:从 Prompt 参数注入到能力分层
java·开发语言·微服务·架构·prompt·agent context
专注仿真9 小时前
引擎扩展性与集成设计深度剖析:从 14 个扩展点到 DIS→HLA 演进之路
开发语言·算法·仿真引擎·推演引擎
Best-Wishes9 小时前
科来网络分析系统(技术交流版)使用说明书
开发语言·junit·php
代码不停10 小时前
自动化测试(java)
java·开发语言·selenium·自动化
Smoothcloud润云10 小时前
PyTorch大模型权重加载与启动耗时优化实战:定位磁盘、内存与显存瓶颈
人工智能·pytorch·python·深度学习·gpu算力
databook10 小时前
从循环到向量化:NumPy提速的7个关键思维
python·数据分析·numpy
棉猴11 小时前
玩游戏学Python1-Python和pgzero库的安装
python·pygame·玩游戏·pgzero·pgzrun
诺伦11 小时前
GEO优化数据实证:78%引用流向官网的机制拆解与小品牌三步管线实战教程 | RiseClaw玄策
python·seo·geo·内容营销·ai搜索