Программирование PythonКонкурентность и asyncioPython-разработчик серверных приложений

В сервисе несколько связанных корутин запускают через asyncio.TaskGroup: одна завершается исключением. Как ...

В сервисе несколько связанных корутин запускают через asyncio.TaskGroup: одна завершается исключением. Как TaskGroup определяет судьбу остальных задач?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Если дочерняя задача в TaskGroup завершается необработанным исключением, группа отменяет ещё не завершившиеся соседние задачи, дожидается их завершения и затем выбрасывает ExceptionGroup. Это обеспечивает согласованное завершение набора связанных задач, а не оставляет фоновые задачи выполняться независимо.

Исторический контекст

TaskGroup появился в Python 3.11 как стандартный инструмент структурированной конкурентности. Он решает распространённую проблему ручного управления задачами: при исключении одной задачи остальные могли продолжать работу, а вызывающему коду приходилось самостоятельно отслеживать их жизненный цикл и ошибки.

Постановка проблемы

Представим операции, которые образуют одну логическую транзакцию: параллельные запросы к нескольким сервисам, обработка частей одного задания или несколько этапов подготовки ответа. Если один этап безвозвратно завершился ошибкой, продолжение остальных может создать лишние побочные эффекты, нагрузку или частично согласованное состояние.

Неверное решение — создать задачи и не дождаться их всех. Тогда исключения могут быть обнаружены поздно, а завершение родительской операции не будет означать завершение дочерних работ.

Подробное решение

Вход в TaskGroup создаёт область, которая владеет дочерними задачами. Выйти из этой области можно только после завершения всех её задач или обработки возникшей ошибки.

import asyncio async def main(): async with asyncio.TaskGroup() as group: group.create_task(load_profile()) group.create_task(load_orders()) group.create_task(load_recommendations())

Если одна задача выбрасывает обычное исключение, TaskGroup инициирует отмену остальных активных задач. Отмена не убивает их мгновенно: каждая задача должна получить CancelledError в подходящей точке приостановки и корректно завершить освобождение ресурсов в finally.

После этого группа дожидается всех дочерних задач. Исключения, возникшие у задач, объединяются в ExceptionGroup, который можно обрабатывать через except*. Исключение из тела блока async with также учитывается при формировании группы, если оно не является обычной отменой.

CancelledError имеет особый статус: отмена одной дочерней задачи сама по себе обычно не считается ошибкой, которая должна отменить соседей. Но если задача из-за отмены не завершает необходимые действия или другая задача выдаёт обычное исключение, группа всё равно дожидается завершения отменённых задач.

Сравнение с asyncio.gather важно: gather по умолчанию передаёт первое исключение вызывающему коду, но не обязан отменять остальные уже запущенные задачи. Поэтому TaskGroup предпочтительнее, когда задачи образуют единое целое и должны иметь согласованный жизненный цикл.

Исключения нельзя бездумно подавлять внутри дочерней корутины. Если обработчик перехватывает CancelledError, он должен корректно завершить работу; в редких случаях, когда отмену намеренно подавляют, может потребоваться также использовать uncancel(), иначе внутреннее состояние отмены способно повлиять на последующее поведение задачи.

Ситуация из практики

Сервис формирует отчёт, одновременно загружая профиль пользователя, платежи и права доступа. Если загрузка прав завершилась ошибкой, продолжать загрузку остальных данных бессмысленно: результат отчёта всё равно нельзя безопасно отдавать.

Вариант с отдельными create_task требует вручную отменять все задачи, ждать их завершения и собирать исключения. Он гибок, но легко приводит к утечке фоновых задач или пропущенным ошибкам.

Вариант с asyncio.gather проще, однако его стандартное поведение не выражает требование «ошибка одного этапа отменяет остальные». Дополнительная ручная логика вокруг gather снова усложняет контроль жизненного цикла.

Выбран TaskGroup: он явно задаёт границы операции, автоматически отменяет ненужные задачи и не завершает блок до их фактического окончания. В результате сервис не продолжает лишние запросы после критической ошибки, а вызывающий код получает структурированный набор исключений.

Что кандидаты часто упускают

  1. Отменяет ли TaskGroup уже завершившиеся задачи?

Нет. Отменяются только ещё активные задачи. Успешно завершившийся результат не откатывается автоматически, поэтому побочные эффекты, выполненные до ошибки соседней задачи, требуют отдельной компенсационной логики.

  1. Что произойдёт, если дочерняя задача поймает своё исключение?

Если корутина обработает исключение и завершится успешно, TaskGroup не увидит сбой и не будет отменять соседей. Это полезно для ожидаемых ошибок, но опасно, если обработчик случайно скрывает критическую неисправность.

  1. Почему после ошибки группа всё равно ждёт отменённые задачи?

Отмена асинхронной задачи кооперативна. Задача может выполнять finally, закрывать соединение или освобождать ресурс, поэтому немедленный выход был бы небезопасен. Ожидание завершения всех дочерних задач гарантирует, что область TaskGroup не покинутa, пока принадлежащие ей работы не завершили очистку.