Программирование SwiftКонкурентностьРазработчик приложений для iOS на Swift

Практическая ситуация: наследует ли дочерняя задача TaskGroup изоляцию MainActor при добавлении из метода, ...

Практическая ситуация: наследует ли дочерняя задача TaskGroup изоляцию MainActor при добавлении из метода, изолированного MainActor?

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

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

Не сама TaskGroup, а замыкание, переданное в addTask, может унаследовать изоляцию MainActor из контекста, в котором оно создано. Если компилятор выводит для этого замыкания изоляцию MainActor, обращения к изолированному состоянию внутри него выполняются в этой изоляции; иначе дочерняя задача не становится автоматически MainActor-изолированной.

Даже при наличии изоляции это не означает постоянное выполнение на одном физическом потоке: после await задача может приостановиться, а продолжение будет запланировано соответствующим исполнителем актора.

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

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

При этом жизненный цикл задачи и её изоляция — разные свойства. Группа управляет структурой и завершением задач, но не превращает все их операции в работу на конкретном акторе.

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

Метод, изолированный MainActor, часто запускает параллельную обработку через TaskGroup. Ошибка возникает, когда разработчик считает, что сам факт создания группы либо гарантирует выполнение всех дочерних операций на MainActor, либо, наоборот, полностью снимает исходную изоляцию.

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

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

Параметр-замыкание addTask имеет изоляцию, которую Swift может вывести из контекста создания. Поэтому замыкание, созданное непосредственно в MainActor-изолированном методе и обращающееся к MainActor-состоянию, может быть MainActor-изолированным.

Минимальный пример:

@MainActor final class ScreenModel { var values: [Int] = [] func load() async { await withTaskGroup(of: Int.self) { group in group.addTask { 42 } for await value in group { values.append(value) } } } }

Здесь обработка values.append выполняется в изоляции MainActor, потому что метод load и его состояние изолированы этим глобальным актором. Само вычисление внутри дочерней задачи не следует считать гарантированно выполняющимся на главном потоке: для CPU-затратной работы нужно явно отделять изолированную часть от фоновой обработки и возвращать на MainActor только результат.

Важное ограничение — изоляция зависит от контекста и типов замыканий, а не от названия TaskGroup. Если дочернее замыкание передаётся через не изолированный слой или его изоляция явно не выводится как MainActor, доступ к MainActor-состоянию потребует корректного изолированного вызова.

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

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

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

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

  1. Означает ли MainActor-изоляция выполнение на главном потоке без исключений?

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

  1. Наследует ли дочерняя задача отмену родителя при использовании TaskGroup?

Да, дочерние задачи структурированной группы связаны с родителем. Отмена родительской задачи делает дочерние задачи отменёнными, но сама отмена является кооперативной: операция должна проверять состояние отмены или вызвать API, который реагирует на неё.

  1. Можно ли считать MainActor-изолированную дочернюю задачу подходящим местом для параллельного ускорения?

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