В чем разница между asyncio.TaskGroup и asyncio.gather?

Ответ

Обе конструкции, asyncio.TaskGroup и asyncio.gather, предназначены для одновременного выполнения нескольких асинхронных задач, но они предлагают разный подход к управлению задачами и обработке ошибок.

TaskGroup (появился в Python 3.11) реализует концепцию структурного параллелизма.

Ключевые различия:

1. Обработка ошибок

  • TaskGroup: При возникновении исключения в одной из задач, TaskGroup немедленно отменяет все остальные задачи в группе и выходит из контекста, пробрасывая исключение. Это гарантирует, что группа завершается целиком — либо все успешно, либо с ошибкой.
  • asyncio.gather: По умолчанию, если одна задача падает с ошибкой, gather продолжает выполнять остальные. Ошибка будет выброшена только после завершения всех задач. Поведение можно изменить с помощью флага return_exceptions=True, тогда gather вернет результаты и исключения вместо того, чтобы их пробрасывать.

2. Синтаксис и жизненный цикл

  • TaskGroup: Используется как асинхронный контекстный менеджер (async with). Это четко определяет границы жизни группы задач. Задачи не могут "утечь" за пределы блока async with.
  • asyncio.gather: Это обычная функция, которая принимает корутины или Future и возвращает результат, когда все они завершены.

3. Добавление задач

  • TaskGroup: Задачи добавляются динамически внутри блока с помощью метода create_task().
  • asyncio.gather: Принимает заранее известный список корутин в качестве аргументов.

Примеры кода

asyncio.TaskGroup — надежная обработка ошибок

import asyncio

async def task_that_fails():
    await asyncio.sleep(1)
    raise ValueError("Something went wrong")

async def long_running_task():
    print("Long task started")
    await asyncio.sleep(5)
    print("Long task finished") # Этот код не выполнится

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(task_that_fails())
            tg.create_task(long_running_task())
    except* ValueError as e:
        print(f"Caught exception: {e.exceptions[0]}")

# Вывод: Long task started -> Caught exception: ValueError('Something went wrong')
# Вторая задача была отменена.
asyncio.run(main())

asyncio.gather — выполнение до конца

async def main_gather():
    try:
        # gather дождется выполнения long_running_task
        await asyncio.gather(
            task_that_fails(),
            long_running_task()
        )
    except ValueError as e:
        print(f"Caught exception: {e}")

# Вывод: Long task started -> Long task finished -> Caught exception: Something went wrong
asyncio.run(main_gather())

Вывод:

  • Используйте TaskGroup для связанных задач, где сбой одной должен приводить к отмене всей операции. Это более современный и безопасный подход.
  • Используйте asyncio.gather для простого параллельного запуска независимых задач, когда вам нужно дождаться их всех, даже если некоторые завершатся с ошибкой.