3 次浏览0 条回复

多个协程并发运行时,如果其中一个失败,其余任务通常不应继续悄悄运行。Python 3.11 的 asyncio.TaskGroup 会在首个非取消异常出现后取消同组其他任务,等待它们完成清理,再以 ExceptionGroup 汇总异常。

环境:Python 3.11+;Linux、macOS 或 Windows;仅使用标准库。

创建 task_group_demo.py

import asyncio


async def worker(
    name: str,
    delay: float,
    *,
    fail: bool = False,
) -> str:
    try:
        await asyncio.sleep(delay)
        if fail:
            raise RuntimeError(f"{name} failed")
        print(f"{name}: done")
        return name
    finally:
        print(f"{name}: cleanup")


async def main() -> None:
    try:
        async with asyncio.TaskGroup() as group:
            fast = group.create_task(worker("fast", 0.05))
            group.create_task(worker("broken", 0.10, fail=True))
            slow = group.create_task(worker("slow", 1.00))
    except* RuntimeError as errors:
        for error in errors.exceptions:
            print("caught:", error)

    print("fast result:", fast.result())
    print("slow cancelled:", slow.cancelled())


asyncio.run(main())

运行:

python task_group_demo.py

预期输出:

fast: done
fast: cleanup
broken: cleanup
slow: cleanup
caught: broken failed
fast result: fast
slow cancelled: True

broken 抛出异常后,尚未完成的 slow 会收到取消请求,但它的 finally 仍会执行。退出 TaskGroup 上下文时,所有子任务都已经结束,因此不会留下后台任务。

这里使用 except* RuntimeError,是因为任务组可能同时收集多个任务异常并抛出 ExceptionGroup。清理代码若捕获 asyncio.CancelledError,通常应在释放资源后继续抛出;吞掉取消可能破坏任务组的收尾语义。任务结果也应在退出任务组后读取,且只对已成功完成的任务调用 result()