Когда объект Swift, запустивший долгоживущую обычную Task с сильным захватом self, сможет деинициализироваться?
Объект не сможет деинициализироваться, пока выполняющаяся Task удерживает его через сильный захват self. Если задача бесконечная или ожидает событие без завершения, возникает фактический цикл удержания: объект владеет задачей, задача владеет замыканием, а замыкание владеет объектом.
Сброс ссылки на Task не отменяет её автоматически. Для разрыва такой связи нужно не захватывать объект сильно либо явно организовать отмену и завершение задачи.
В Swift существуют структурированные задачи, например async let и TaskGroup, жизненный цикл которых связан с областью видимости. Однако обычная Task создаёт неструктурированную дочернюю работу: она может продолжаться после возврата из функции, создавшей её.
Такой подход нужен для долгоживущих операций, например наблюдения за событиями или фоновой синхронизации. Обратная сторона — разработчик самостоятельно отвечает за связь жизненного цикла задачи с объектом-владельцем.
Рассмотрим объект, который сохраняет задачу в свойстве. Если тело задачи обращается к self, замыкание обычно захватывает объект сильно. Тогда объект не освобождается, потому что его свойство удерживает задачу, а задача удерживает замыкание с объектом.
Особенно опасны бесконечные циклы, ожидание потока событий и операции, которые не реагируют на отмену. Удаление внешней ссылки на объект в такой ситуации не вызывает его deinit, а значит, не срабатывает код очистки ресурсов.
Обычная Task продолжает выполняться независимо от того, хранится ли где-либо её дескриптор. Дескриптор позволяет запросить результат или вызвать отмену, но его уничтожение само по себе не является сигналом отмены.
При сильном захвате self задача удерживает объект до завершения замыкания. После завершения задачи захват освобождается, и объект может деинициализироваться, если других ссылок нет.
Безопасный вариант — слабый захват и проверка отмены:
Здесь задача не удерживает self постоянно. guard let self создаёт временное сильное удержание на одну итерацию, поэтому объект может быть освобождён между итерациями, если внешние ссылки исчезли.
Отмена в Swift кооперативная: cancel() только устанавливает состояние отмены и уведомляет ожидающие операции. Цикл, блокирующий вызов или сторонний API должны самостоятельно проверять отмену либо корректно завершаться после неё.
Task.detached не является автоматическим исправлением: при сильном захвате объект всё равно может удерживаться. Кроме того, detached-задача не наследует контекст текущей задачи и предъявляет более строгие требования к передаваемым данным, поэтому её нельзя выбирать только ради решения проблемы времени жизни.
В контроллере экрана запускается задача, которая постоянно читает поток обновлений. В первом варианте задача сильно захватывает контроллер, а контроллер хранит её дескриптор. При закрытии экрана контроллер не деинициализируется, поток продолжает работать, а связанные ресурсы остаются заняты.
Вариант с безусловным вызовом cancel() при закрытии недостаточен, если задача не проверяет отмену или зависший API не возвращает управление. Вариант с Task.detached также не решает сильный захват и усложняет передачу данных между изоляциями.
Выбранное решение — слабый захват контроллера, отменяемый поток событий и явное хранение задачи в объекте-владельце. При прекращении работы задача получает отмену, выходит из цикла, освобождает временные ссылки, а контроллер и его ресурсы корректно деинициализируются.
Нет. Слабый захват предотвращает удержание объекта замыканием, но не заставляет задачу завершиться. Если замыкание продолжает бесконечно ждать внешний источник, оно само может оставаться активным, хотя объект уже освобождён.
Нужно сочетать слабый захват с проверкой Task.isCancelled, отменяемыми операциями ожидания или явным сигналом завершения. Слабая ссылка решает проблему удержания объекта, но не проблему жизненного цикла самой задачи.
Объект будет удерживаться до окончания этой итерации и всех вызовов, которые используют локальную сильную ссылку. Если такой вызов надолго приостанавливается на await, объект может оставаться живым всё это время.
Это обычно приемлемо, если каждая итерация ограничена по времени и корректно реагирует на отмену. Нельзя считать слабый захват абсолютной гарантией немедленной деинициализации объекта.
Потому что до уничтожения объект должен сначала освободиться, а сильный захват из тела задачи может препятствовать этому. Свойство с дескриптором не задаёт автоматического правила «уничтожение владельца отменяет задачу».
Такое правило нужно реализовать явно: использовать слабый захват, вызвать cancel() в подходящем жизненном цикле и обеспечить кооперативное завершение работы. При этом deinit не может быть надёжным способом разорвать цикл, если сам цикл не позволяет deinit наступить.