(九)任务组、结果与异常传播
1. 把一组相关操作放进同一作用域
# 项目文件:demos/task_group_results.py
# async with 离开前等待组内任务结束,之后 result 读取已完成的值。
import asyncio
async def twice(value):
await asyncio.sleep(0)
return value * 2
async def main():
async with asyncio.TaskGroup() as group:
tasks = [group.create_task(twice(value)) for value in [20, 35, 45]]
print([task.result() for task in tasks])
if __name__ == "__main__":
asyncio.run(main())输出 [40, 70, 90]。async with 与前面的 with 一样标出使用范围,但进入、退出动作可以进行异步等待。TaskGroup 离开前收集组内任务,因此可以在外面读取这些已完成的结果。
这里的 task.result() 只读取已经完成的值;任务尚未完成时会抛出 InvalidStateError,不会像线程池的 future.result() 那样阻塞等待。需要等待 Task 时使用 await task,得到执行机会后再继续调用者的下一句。
2. 一个任务失败后先收尾同组任务
# 项目文件:demos/task_group_failure.py
# 等待项启动后,另一个任务才抛错;任务组取消等待项,等 finally 完成后向外报告。
import asyncio
async def waiting(ready, trace):
try:
trace.append("等待项开始")
ready.set()
await asyncio.Event().wait()
finally:
trace.append("等待项收尾")
async def failing(ready, trace):
await ready.wait()
trace.append("失败项抛错")
raise ValueError("书目格式错误")
async def main():
ready, trace = asyncio.Event(), []
try:
async with asyncio.TaskGroup() as group:
group.create_task(waiting(ready, trace))
group.create_task(failing(ready, trace))
except* ValueError as errors:
print("收到错误", [str(error) for error in errors.exceptions])
print(trace)
if __name__ == "__main__":
asyncio.run(main())asyncio.Event 用来通知同一循环中的任务:ready 发出后,失败项开始抛错。输出错误 书目格式错误,再看到 ['等待项开始', '失败项抛错', '等待项收尾']。任务组没有在收到第一次失败后把另一项留在后台不管。
TaskGroup 中有普通错误时,会取消尚未结束的同组任务,等待它们退出,再把错误组成 ExceptionGroup 向外传播。Python 3.12 中的 except* ValueError 匹配异常组里相应类型的成员;本例的 .exceptions 里只有一个直接的 ValueError。它不同于普通 except 接住单个调用错误。
3. 为独立任务保留逐项结果
有些任务要全部尝试,分别报告成功和失败,可以收集异常作为结果:
# 项目文件:demos/gather_errors.py
# return_exceptions=True 保留各项结果;调用者逐项决定如何显示或继续处理。
import asyncio
async def check_minutes(value):
await asyncio.sleep(0)
if value < 0:
raise ValueError("分钟数为负")
return value * 2
async def main():
results = await asyncio.gather(
check_minutes(20), check_minutes(-1), return_exceptions=True
)
for result in results:
if isinstance(result, Exception):
print("错误", result)
else:
print("结果", result)
if __name__ == "__main__":
asyncio.run(main())输出 结果 40 与 错误 分钟数为负。默认 gather 不把普通异常当作返回值,第一次异常会向调用者传播,但不会因此自动取消其他已经交给 gather 的任务。TaskGroup 则把同组任务的生命周期绑在一起。本章书目导入需要完整数据,选择任务组;独立批量检查可以另存逐项失败记录。