好的,我们来精确地分析在您提供的 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: 这个块会被执行,需要同时满足以下两个条件:
-
任务必须已经被标记为"取消" : 在
long_running_task外部的某个地方,必须已经调用了task.cancel()。这就像"解雇通知"已经被递送到了员工的桌上。 -
控制流必须位于
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()。
long_running_task开始执行,打印"任务开始..."、"步骤 1..."。- 执行
time.sleep(2)。在这 2 秒内,main函数已经调用了task.cancel()。现在,task对象内部的"取消"标志位已经被设置为True。但是,long_running_task对此一无所知 ,因为它正被一个同步的sleep阻塞,没有机会与事件循环交互。 time.sleep(2)结束,打印"步骤 2..."。- 代码执行到
await asyncio.sleep(1)。这是关键点!await表达式将控制权交还给事件循环。- 事件循环在准备执行
asyncio.sleep(1)之前,会检查这个task的状态。 - 它发现:"哦,这个任务的取消标志位是
True!" - 于是,事件循环决定不执行
asyncio.sleep(1)。它立即在这个await的位置抛出一个asyncio.CancelledError。
- 这个
CancelledError异常被try...except块捕获。 - 程序的控制流立即跳转 到
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 异常处理和任务取消的关键。