Практическая ситуация: родительская задача Swift отменена во время выполнения дочерней — какое изменение со...

Практическая ситуация: родительская задача Swift отменена во время выполнения дочерней — какое изменение состояния дочерней задачи происходит автоматически?

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

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

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

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

Структурированная конкурентность появилась как способ связать жизненный цикл дочерних задач с областью выполнения родителя. Она решает проблему «осиротевших» задач, которые продолжают работать после завершения или отмены операции, породившей их.

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

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

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

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

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

В структурированной конкурентности отмена распространяется от родителя к дочерним задачам. Это касается задач, созданных через async let и TaskGroup. Дочерняя задача получает признак отмены, но Swift не прерывает произвольный синхронный код и не завершает задачу насильно.

Задача должна сама реагировать на отмену. Для этого применяются проверка Task.isCancelled, вызов Task.checkCancellation() или отменочувствительные async-операции. Task.checkCancellation() выбрасывает CancellationError, поэтому вызывающий код должен корректно обработать или передать эту ошибку.

func loadScreen() async throws -> [Item] { try await withThrowingTaskGroup(of: [Item].self) { group in group.addTask { try await loadItems() } group.addTask { try await loadRecommendations() } var result: [Item] = [] for try await items in group { try Task.checkCancellation() result += items } return result } }

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

Важно отличать структурированные дочерние задачи от неструктурированной Task. Задача, созданная отдельно через Task, не становится автоматически дочерней задачей в том же смысле и не получает автоматическую отмену от произвольного родителя; её жизненный цикл нужно контролировать явно. Принудительно остановить такую работу нельзя, поэтому обычно хранят дескриптор задачи и вызывают у него cancel().

Компромисс кооперативной отмены — предсказуемость против скорости остановки. Она безопасна для состояния, но плохо работающий или полностью блокирующий код может долго игнорировать отмену.

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

В поисковом экране каждая смена текста запускает структурированную задачу, которая параллельно получает подсказки и историю запросов. При вводе нового символа предыдущая родительская задача отменяется.

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

Второй вариант — использовать async let или TaskGroup внутри одной родительской задачи. Отмена родителя распространяется на дочерние операции, а ожидание завершения группы сохраняет структурированный жизненный цикл. Выбирается этот вариант; дополнительно сетевой слой должен поддерживать отмену или хотя бы регулярно проверять её.

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

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

  1. Отмена родителя немедленно прерывает дочерний синхронный код?

Нет. Отмена только устанавливает состояние отмены. Длинный цикл без точек приостановки и без проверки Task.isCancelled продолжит выполняться, поэтому CPU-вычисления должны явно содержать безопасные точки проверки.

  1. Обязана ли отменённая дочерняя задача выбросить CancellationError?

Нет. Ошибка возникает только если код сам вызывает Task.checkCancellation() или использует API, которое выбрасывает её при отмене. Задача может обнаружить отмену через Task.isCancelled, корректно завершить работу и вернуть обычный результат, хотя для большинства отменяемых async-операций выброс ошибки делает контракт яснее.

  1. Распространяется ли отмена родителя на задачу, созданную через обычную Task внутри его тела?

Не как структурированная отмена дочерней задачи. Такая задача наследует некоторые свойства контекста, но её жизненный цикл не привязан к области родителя автоматически. Если она должна завершаться вместе с операцией, следует использовать async let или TaskGroup; иначе нужно сохранить дескриптор и явно управлять его отменой.