PythonMiddleTechnical
Что такое TaskGroup и чем он отличается от gather()?
TaskGroup (Python 3.11+) даёт structured concurrency: задачи живут в блоке async with, при ошибке одной остальные отменяются, исключения приходят как ExceptionGroup. gather проще для фиксированного списка результатов, но не контролирует lifetime.
Что такое TaskGroup
asyncio.TaskGroup (Python 3.11+) — реализация structured concurrency: дочерние задачи живут только в пределах блока async with. На выходе из блока:
- группа ждёт завершения всех созданных задач;
- если любая задача падает, остальные получают
cancel(); - исключения собираются в
ExceptionGroupи поднимаются на границе блока.
Задачи создаются методом tg.create_task(coro, name=...). Возвращает обычный asyncio.Task, но его lifetime связан с группой.
Сравнение с gather
| Аспект | TaskGroup | asyncio.gather |
|---|---|---|
| Lifetime | привязан к блоку | зависит от вызывающего |
| Поведение при ошибке | отменяет остальные, поднимает ExceptionGroup | с return_exceptions=False поднимает первое; остальные продолжают |
| Список результатов | через task.result() | возвращает список в порядке аргументов |
| Динамическое добавление | да, через create_task | нет, фиксированный список |
Пример
import asyncio
async def fetch(name: str, delay: float) -> str:
await asyncio.sleep(delay)
if name == "bad":
raise RuntimeError("boom")
return name
async def main() -> None:
try:
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(fetch("a", 0.1), name="fetch-a")
t2 = tg.create_task(fetch("b", 0.2), name="fetch-b")
t3 = tg.create_task(fetch("bad", 0.05), name="fetch-bad")
print(t1.result(), t2.result())
except* RuntimeError as eg:
for exc in eg.exceptions:
print("failed:", exc)
asyncio.run(main())
Конструкция try: ... except* ExceptionType: — except-group syntax из PEP 654, обрабатывает только нужный тип внутри группы.
Когда какой инструмент
- TaskGroup: фан-аут с гарантией, что фоновые задачи не переживут вызывающий код; динамическое добавление задач (например, обход графа); явная обработка нескольких ошибок.
- gather: простая агрегация фиксированного списка корутин с упорядоченным результатом, особенно с
return_exceptions=True, когда падения одного не должны отменять других. - asyncio.wait: когда нужны
FIRST_COMPLETED/FIRST_EXCEPTIONи ручное управление неотмененным остатком.
Подводные камни
- Создавать задачи через
asyncio.create_taskвне группы и забывать про lifetime — задача «утекает», ошибка теряется вTask exception was never retrieved. - Не быть готовым ловить
ExceptionGroup: обычныйexcept RuntimeErrorне сработает на группу — нуженexcept*илиexcept ExceptionGroup. - Полагаться на
return_exceptions=True-семантику gather внутри TaskGroup — её там нет, любая необработанная ошибка отменит соседей. - Считать, что отмена ждёт мгновенно: cancelled task всё ещё доходит до ближайшего
await, поэтому критические секции защищайтеasyncio.shieldилиfinally. - Использовать TaskGroup на Python 3.10 и ниже — доступно только с 3.11.
- Не давать задачам имя через
name=— отладкаasyncio.all_tasks()превращается в кашу изTask-42. - Создавать TaskGroup в long-lived воркере без выхода из блока — она по сути не используется, проще
create_taskс явным supervisor-ом.
Common mistakes
- Говорить, что TaskGroup просто новый синтаксис gather.
- Не знать про отмену sibling tasks.
- Не понимать async with lifecycle.
What the interviewer is testing
- Объясняет structured concurrency.
- Знает поведение при исключении.
- Может выбрать gather или TaskGroup по задаче.