В чём принципиальное преимущество TaskGroup перед async let при динамическом числе дочерних операций?
TaskGroup подходит, когда количество дочерних задач определяется во время выполнения: их можно добавлять динамически, например в цикле по полученному списку. async let предназначен для фиксированного набора дочерних операций, который задаётся структурой исходного кода.
Оба механизма сохраняют структурированную конкурентность: дочерние задачи привязаны к области действия родителя, а выход из этой области не происходит, пока они не завершены.
Структурированная конкурентность появилась как способ решить проблему неуправляемых фоновых задач: их жизненного цикла, отмены, обработки ошибок и утечек ресурсов. Вместо разрозненного запуска задач Swift связывает дочерние задачи с родительской областью выполнения.
async let закрывает простой случай: несколько заранее известных операций можно запустить конкурентно и затем дождаться их результатов. TaskGroup дополняет этот подход для алгоритмов, где число операций заранее неизвестно или формируется на основе входных данных.
Представим загрузку данных для списка идентификаторов, полученного от сервера. До выполнения запроса неизвестно, сколько элементов окажется в списке, поэтому заранее объявить отдельную переменную async let для каждого результата невозможно.
Запускать независимые задачи без структурированной связи с родителем тоже рискованно: их жизненный цикл придётся контролировать вручную, а ошибки и отмену — согласовывать самостоятельно. Неверный выбор может привести к преждевременному возврату из функции, потерянным ошибкам или задачам, продолжающим работу после завершения операции-владельца.
TaskGroup предоставляет область, внутри которой дочерние задачи добавляются вызовами addTask. Количество таких вызовов может зависеть от входных данных, результатов предыдущих вычислений или условий выполнения.
Каждая добавленная задача является дочерней для группы. Группа не завершается, пока её дочерние задачи не закончат работу; это сохраняет структурированность и не позволяет родительской функции незаметно оставить такие задачи выполняться в фоне.
Для обработки ошибок применяют withThrowingTaskGroup. Ошибка дочерней задачи может привести к завершению групповой операции с ошибкой и отмене ещё выполняющихся дочерних задач, но отмена не означает мгновенное физическое прекращение их работы: задачи должны реагировать на отмену кооперативно.
async let лучше использовать, когда набор операций фиксирован и известен из кода: например, параллельная загрузка профиля и настроек. Он проще читается и не требует ручного добавления дочерних задач, но не предоставляет динамического создания произвольного количества операций.
TaskGroup не гарантирует сохранение исходного порядка элементов при сборе результатов: если порядок важен, его нужно восстановить самостоятельно, например сохраняя исходный индекс вместе с результатом. Также группа не является универсальным способом ограничить число одновременно выполняемых операций; для этого нужен отдельный механизм ограничения параллелизма.
Сервис получает список из нескольких тысяч файлов и должен загрузить их конкурентно. Число файлов известно только после ответа сервера, поэтому набор async let неприменим.
Первый вариант — создать обычную Task для каждого файла и хранить их отдельно. Это даёт гибкость, но усложняет ожидание всех задач, отмену, обработку ошибок и освобождение ресурсов.
Второй вариант — использовать TaskGroup и добавлять задачи по мере обхода списка. Он сохраняет связь с родительской операцией, автоматически ожидает дочерние задачи при завершении области и позволяет централизованно обрабатывать результаты и ошибки.
Выбранный вариант — TaskGroup, но с дополнительным ограничением числа одновременно обрабатываемых файлов. Сам по себе TaskGroup допускает создание большого числа дочерних задач, поэтому для контроля нагрузки применяют пакетную обработку, очередь или семафороподобный асинхронный ограничитель. В результате жизненный цикл задач остаётся структурированным, а внешняя система не перегружается.
Вопрос 1. Можно ли добавлять новые дочерние задачи в TaskGroup после начала чтения результатов?
Да. Пока область группы ещё активна, новые задачи можно добавлять, в том числе после получения результата уже завершившейся дочерней задачи. Это позволяет строить динамические схемы обработки, например добавлять новые элементы после обхода промежуточного результата.
Однако после выхода из области группы добавление невозможно: группа завершена, а все её дочерние задачи должны быть учтены. Поэтому динамическое расширение допустимо только внутри жизненного цикла группы.
Вопрос 2. Чем отличается отмена TaskGroup от немедленного убийства всех его дочерних задач?
Отмена распространяется на дочерние задачи как состояние отмены, но Swift не прерывает произвольный синхронный код принудительно. Задача должна периодически проверять отмену или использовать отменяемые операции, которые сами реагируют на неё.
Если дочерняя задача выполняет длительную блокирующую работу, отмена может не дать быстрого эффекта. Поэтому конкурентный код должен избегать блокирующих участков или выносить их в подходящий исполнитель и обеспечивать явную проверку состояния отмены.
Вопрос 3. Что произойдёт с несобранным результатом дочерней задачи при завершении TaskGroup?
Результат не становится автоматически доступным вызывающему коду. Его нужно получить через итерацию группы или другой предусмотренный механизм сбора результатов и сохранить в собственную структуру данных.
Если результат не был обработан, он фактически теряется после завершения соответствующей задачи. При этом завершение группы всё равно учитывает саму дочернюю задачу, поэтому забытый результат не позволяет оставить её выполняться за пределами области группы.