От чего зависит поток выполнения deinit, когда последняя сильная ссылка освобождается в фоне?

От чего зависит поток выполнения deinit, когда последняя сильная ссылка освобождается в фоне?

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

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

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

Поэтому deinit нельзя использовать как место для UI-операций или другой очистки, требующей конкретного потока. Если такая гарантия нужна, очистку следует явно организовать в требуемом контексте.

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

До ARC разработчик вручную управлял парами удержания и освобождения объектов. Момент вызова освобождения фактически определял момент, когда объект мог быть уничтожен, поэтому поток выполнения очистки зависел от места последнего освобождения.

ARC автоматизирует вставку операций управления временем жизни, но не меняет фундаментальный принцип: объект уничтожается, когда исчезает последняя сильная ссылка. Автоматизация не означает привязку deinit к потоку создания или к главному потоку.

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

Объект может иметь сильные ссылки одновременно из нескольких потоков: например, из свойства контроллера, очереди фоновой операции и замыкания. Когда одна из ссылок исчезает, объект ещё жив, поэтому deinit не вызывается.

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

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

ARC поддерживает учёт сильных ссылок на экземпляр. Каждое создание или копирование сильной ссылки увеличивает этот учёт, а её уничтожение или переназначение уменьшает его. Когда учёт достигает нуля, экземпляр становится недоступен для дальнейшего использования, вызывается его deinit, затем освобождается память объекта.

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

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

weak-ссылки не удерживают объект и при уничтожении объекта автоматически обнуляются. unowned-ссылки также не продлевают время жизни, но не обнуляются; их использование после уничтожения объекта приводит к ошибке времени выполнения. Ни один из этих видов ссылок не гарантирует поток вызова deinit.

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

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

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

Вариант с очисткой только в deinit прост, но его минус — очистка может произойти на фоне. Вариант с принудительным вызовом deinit невозможен: жизненным циклом экземпляра управляет ARC. Вариант с явным методом close, вызываемым на главном потоке, лучше разделяет управление ресурсом и временем жизни объекта.

Выбранное решение — сделать close идемпотентным, вызвать его при закрытии экрана в требуемом контексте, а в deinit оставить защитную очистку для случаев, когда close не был вызван. В результате ресурс закрывается предсказуемо, а позднее уничтожение объекта фоновой ссылкой не нарушает требования к потоку.

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

  1. Гарантирует ли вызов метода объекта выполнение deinit на том же потоке?

Нет. Вызов метода лишь временно использует уже существующую сильную ссылку или другой способ доступа к объекту. После завершения метода могут оставаться ссылки из других потоков, а последняя ссылка может быть освобождена совсем в другом месте.

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

  1. Можно ли гарантировать главный поток через weak-ссылку на объект?

Нет. weak влияет на владение, но не на поток. Чтение weak-ссылки временно получает сильную ссылку на объект, если он ещё жив, однако после завершения этого использования последняя сильная ссылка всё равно может быть освобождена на любом потоке.

weak полезна для предотвращения циклов владения, но не заменяет синхронизацию и не задаёт контекст выполнения deinit.

  1. Как спроектировать класс, если его ресурс обязательно должен освобождаться на главном потоке?

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

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