В сервисе нужно понять, продолжится ли независимая задача после сбоя соседней. Как поведёт себя asyncio.gather по умолчанию в этом коде?
import asyncio
async def job(name, delay, fail=False):
await asyncio.sleep(delay)
if fail:
raise ValueError(name)
print(name)
return name
async def main():
tasks = [
asyncio.create_task(job("A", 0.1, True)),
asyncio.create_task(job("B", 0.2)),
]
try:
await asyncio.gather(*tasks)
except ValueError:
print("gather failed")
await asyncio.sleep(0.2)
print(tasks[1].done())
asyncio.run(main())
asyncio.gather по умолчанию сразу передаёт вызывающему коду первое исключение, но не отменяет остальные уже запущенные задачи. Поэтому сначала будет напечатано gather failed, затем завершится задача B, будет напечатано B, а последняя строка выведет True.
Исключение из A не превращается автоматически в отмену B. Если требуется связать жизненный цикл задач и отменять остальные при ошибке одной из них, следует использовать asyncio.TaskGroup или явно реализовать такую политику.
Асинхронные библиотеки часто запускают несколько независимых операций одновременно: сетевые запросы, чтение из разных источников или параллельную обработку сообщений. Для этого нужен примитив, который соберёт результаты нескольких ожидаемых объектов в одной корутине.
asyncio.gather решает задачу координации ожидания, но исторически ориентирован прежде всего на сбор результатов, а не на строгую структурированную обработку ошибок. Поэтому его политика исключений отличается от политики современных средств структурированной конкурентности, таких как TaskGroup.
Если одна операция завершается ошибкой, разработчик должен понимать, что происходит с остальными: они продолжаются, отменяются или их результаты теряются. Неверное предположение может привести к незавершённым побочным действиям, лишним запросам или преждевременному закрытию ресурсов.
В приведённом коде задача A завершается через 0,1 секунды с исключением. gather немедленно завершает своё ожидание ошибкой, но задача B уже создана и продолжает выполняться независимо от того, что вызывающий код перешёл в except.
По умолчанию у asyncio.gather параметр return_exceptions=False. При первом исключении он распространяет это исключение в ожидающую корутину. Это не означает автоматическую отмену остальных awaitable-задач.
После обработки ValueError корутина main продолжает выполнение и ждёт ещё 0,2 секунды. За это время B выходит из sleep, печатает B и успешно завершается. Поэтому tasks[1].done() возвращает True.
Результаты успешных задач при обычном режиме не возвращаются, если одна из задач вызвала исключение. Если передать return_exceptions=True, исключения попадут в результирующий список как объекты, а gather дождётся всех awaitable:
Этот режим удобен, когда ошибки отдельных операций являются ожидаемыми данными и нужно получить полный отчёт. Но он опасен, если исключение нельзя молча смешивать с обычным результатом: вызывающий код обязан проверить элементы списка.
Если вызывающий код сам отменяет gather, отмена распространяется на ещё не завершённые операции, переданные ему. Это отличается от ситуации, когда один из внутренних awaitable завершился исключением: такое исключение само по себе остальные операции не отменяет.
В обработчике нужно запросить профиль пользователя и список рекомендаций. Вариант с gather позволяет быстро вернуть ошибку профиля, пока запрос рекомендаций продолжает выполняться. Плюс — простая модель и отсутствие отмены потенциально полезной независимой работы; минус — после ошибки нужно отдельно контролировать фоновые задачи и их побочные эффекты.
Второй вариант — передать операции в TaskGroup. При исключении одной задачи остальные дочерние задачи отменяются, а ошибки сообщаются после завершения группы. Это лучше для связанных операций, которые должны завершиться вместе, но не подходит, если независимую задачу действительно нужно довести до конца.
Практическое решение выбирают по зависимости операций. Для независимых действий используют gather с явной обработкой результатов и очисткой задач, а для единой транзакционной операции — TaskGroup. В обоих случаях важно не оставлять задачи без понятной политики отмены и обработки исключений.
Отменяет ли gather остальные задачи, если одна из них вызвала исключение?
Нет, при стандартном return_exceptions=False исключение распространяется наружу, но уже выполняющиеся остальные awaitable автоматически не отменяются. Они могут продолжить работу и выполнить побочные действия. Отдельно нужно учитывать отмену самого объекта gather: если отменён именно он, незавершённые дочерние операции отменяются.
Что изменяет параметр return_exceptions=True?
gather дожидается всех переданных операций и возвращает список, где успешные результаты смешаны с объектами исключений. Исключение в таком режиме не выбрасывается автоматически в месте await. Это полезно для независимых запросов, но требует явной проверки каждого элемента и не должно применяться для сокрытия программных ошибок.
Почему TaskGroup обычно безопаснее для связанных задач?
TaskGroup задаёт структурированную область жизни дочерних задач. Если одна задача завершается ошибкой, группа отменяет оставшиеся задачи, дожидается их завершения и затем сообщает ошибки вызывающему коду. Это предотвращает ситуацию, когда родительская операция уже завершилась с ошибкой, а связанные дочерние действия продолжаются бесконтрольно; цена такого поведения — невозможность сохранить независимую работу без специального проектирования.