Допустимо ли считать вызов nonisolated метода actor сериализованным самим actor?

Допустимо ли считать вызов nonisolated-метода actor сериализованным самим actor?

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

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

Нет. nonisolated-метод не выполняется в изоляции экземпляра actor, поэтому его вызовы не сериализуются executor’ом этого actor и могут выполняться одновременно. Такой метод обязан быть безопасным независимо от actor-изоляции и не может напрямую обращаться к изолированному изменяемому состоянию actor.

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

Акторы появились в Swift для структурирования конкурентного доступа к изменяемому состоянию без ручных блокировок. По умолчанию их изолированные методы выполняются через executor actor, что исключает одновременное выполнение изолированных участков для одного экземпляра.

Однако некоторым операциям не нужен доступ к состоянию actor: например, чистому вычислению или обработке заранее переданных данных. Для таких случаев существует nonisolated, позволяющий явно исключить метод из actor-изоляции и не платить за ненужную сериализацию.

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

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

Неверное предположение особенно опасно для асинхронного nonisolated-метода: после await он также не обязан возвращаться к executor’у исходного actor. Его продолжения должны рассматриваться как обычный конкурентный код.

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

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

actor Cache { var entries: [String: String] = [:] nonisolated func normalize(_ value: String) -> String { value.trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() } } let cache = Cache() let key = cache.normalize(" Swift ")

Метод normalize не читает entries, поэтому ему не нужна actor-изоляция. Несколько задач могут вызвать его параллельно; actor не будет превращать эти вызовы в последовательную очередь.

Из nonisolated-метода нельзя напрямую читать или изменять изолированное изменяемое свойство actor. Чтобы обработать такое состояние безопасно, нужно оставить метод изолированным либо сначала получить из actor безопасный снимок данных, а затем передать этот снимок в независимую функцию.

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

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

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

Рассматривались два варианта. Возврат метода в actor-изоляцию устранял гонку, но заставлял все запросы конкурировать за один executor. Внешняя блокировка сохраняла nonisolated, однако добавляла ручное управление синхронизацией и риск ошибочного использования.

Выбрали разделение ответственности: actor формировал неизменяемый Sendable-снимок настроек, а nonisolated-функция форматировала только этот снимок и локальные значения. В результате состояние actor осталось защищённым, форматирование стало параллельным, а общий изменяемый кэш был удалён.

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

  1. Обязан ли nonisolated-метод выполняться на том же executor, что и actor?

Нет. Сам факт принадлежности метода типу actor не задаёт ему actor-изоляцию после объявления nonisolated. Планирование происходит по общим правилам Swift Concurrency, поэтому нельзя использовать такой метод как способ принудительно выполнить работу на executor actor.

  1. Можно ли из nonisolated-метода читать неизменяемые данные actor?

Только если это разрешено правилами изоляции и тип значения безопасен для передачи за пределы actor. Неизменяемость сама по себе не делает произвольный ссылочный объект безопасным: let может указывать на объект с изменяемым внутренним состоянием. Поэтому важно оценивать не только возможность записи в свойство, но и безопасность самого значения.

  1. Достаточно ли сделать nonisolated-метод async, чтобы доступ к общему состоянию стал безопасным?

Нет. async позволяет приостанавливать задачу, но не добавляет синхронизацию. Асинхронный nonisolated-метод всё равно не получает автоматически доступ к изолированному состоянию actor и не сериализуется его executor’ом; для общего изменяемого состояния нужна actor-изоляция или отдельный корректный механизм синхронизации.