В чем разница между 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 для простого параллельного запуска независимых задач, когда вам нужно дождаться их всех, даже если некоторые завершатся с ошибкой.

Ответ 18+ 🔞

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

Представь, что TaskGroup — это новый, строгий прораб на стройке (появился в Python 3.11). А gather — это старый, пофигистский бригадир. Сейчас поймёшь, в чём разница.

Главные отличия, или где собака зарыта

1. Как они относятся к косякам (обработка ошибок)

  • TaskGroup: Этот, сука, максималист. Один работяга на стройке уронил кирпич на ногу и заорал — прораб сразу орет "ВСЁ, СТОП, РАЗБЕГАЙСЯ!" и отменяет все остальные задачи. Группа либо вся работает, либо вся летит в тартарары. Чётко, структурно, без сюрпризов.
  • asyncio.gather: А этот старый пердун так не умеет. Один упал с лесов — бригадир продолжает свистеть и остальные работают. Ошибку он тебе покажет только когда ВСЕ достроят, хоть и криво. Можно, конечно, сказать ему return_exceptions=True, тогда он просто принесёт тебе пачку результатов, а в ней — один труп с исключением. Но это уже твои проблемы.

2. Как с ними работать (синтаксис и жизнь)

  • TaskGroup: Работает через async with. Это как зайти на охраняемую территорию — задачи создаются прямо внутри блока методом create_task(). Вышел из блока — всё, задач нет, они не могут сбежать и висеть в памяти как проклятые.
  • asyncio.gather: Просто функция, ей скармливают список корутин заранее и говорят: "Жди всех". Динамически ничего не добавишь.

3. Когда что добавлять

  • TaskGroup: Задачи плодятся прямо по ходу пьесы, динамически. Увидел работу — кинул в группу.
  • asyncio.gather: Нужно заранее знать, кого и сколько ты собрался гонять.

Смотри, как это выглядит в коде

asyncio.TaskGroup — дисциплина и порядок

import asyncio

async def task_that_fails():
    await asyncio.sleep(1)
    raise ValueError("Чё-то пошло не так, ёпта!")

async def long_running_task():
    print("Долгая задача началась")
    await asyncio.sleep(5)
    print("Долгая задача завершилась") # Этой строчке не бывать, увы!

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"Поймал исключение: {e.exceptions[0]}")

# Вывод: "Долгая задача началась" -> "Поймал исключение: ValueError('Чё-то пошло не так, ёпта!')"
# Вторая задача была пристрелена на месте. Жестоко, но справедливо.
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"Поймал исключение: {e}")

# Вывод: "Долгая задача началась" -> "Долгая задача завершилась" -> "Поймал исключение: Чё-то пошло не так, ёпта!"
# Всё выполнилось, но исключение прилетело в конце. Как после вечеринки — весело было, а утром похмелье.
asyncio.run(main_gather())

Итог, или что куда пихать

Короче, выбор простой, как три копейки:

  • Берёшь TaskGroup, когда задачи — как команда: один споткнулся, падают все. Нужна чистая отмена и порядок. Современно, модно, молодёжно.
  • Тащишь asyncio.gather, когда задачи друг про друга не знают и им похуй на соседа. Запустил и забыл, результат соберёшь потом, даже если там одни трупы.

Вот и вся магия, ебать. Выбирай с умом, а то потом дебажить эти гонки и утечки — волосы на жопе поседеют.