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"。
验证方法
直接运行上述代码,观察控制台输出顺序:
- 两个任务先后打印“开始执行”日志;
- 0.5秒后打印“已发送取消请求给任务A”;
- 任务A打印“收到取消信号,执行清理...”和“清理完成,重新抛出取消异常”;
- 任务B在1秒时打印“正常完成”;
- 最终输出状态检查结果。
判定标准:若输出中 任务A cancelled 状态: True、任务B done 状态: True,且任务A的清理日志出现在取消请求之后、状态检查之前,则说明取消机制工作正常。若任务A的 cancelled() 返回 False,说明 except 块中缺少 raise 重新抛出语句。
关键要点
- 取消传播:
cancel()会在任务当前挂起点(如sleep)注入CancelledError,若任务正在执行纯CPU计算(无挂起点),取消需等待下一次await才能生效。 - 必须重新抛出:捕获
CancelledError后若吞掉异常(不raise),任务会被视为正常完成,cancelled()返回False,且外层gather无法感知取消意图。 - 清理时机:在
except块中执行资源释放操作,但应避免在清理中再次await长时间操作,否则会延迟取消完成。