Защищает ли actor глобальную изменяемую переменную от гонок данных?
Нет. Actor изолирует только своё собственное изолированное состояние и не превращает произвольную глобальную переменную в безопасную для конкурентного доступа. Глобальное состояние нужно отдельно изолировать: хранить внутри actor, защищать мьютексом или привязать к глобальному actor.
Actors появились как модель управления разделяемым изменяемым состоянием: вместо ручной синхронизации доступ к состоянию конкретного экземпляра сериализуется самим actor. Это решает проблему гонок, когда несколько задач одновременно читают и изменяют одни данные.
Однако изоляция является свойством конкретной области состояния, а не автоматически всех данных, к которым обращается код actor. Иначе любой вызов из actor мог бы случайно сделать безопасными глобальные переменные, что нарушило бы предсказуемость модели.
Пусть метод actor изменяет глобальную переменную. Один вызов может попасть внутрь actor и выполняться последовательно с другими вызовами этого же actor, но другой код способен обратиться к глобальной переменной напрямую.
В результате доступ через actor и прямой доступ могут выполняться одновременно. Это создаёт гонку данных, даже если все методы самого actor вызываются безопасно.
При строгой проверке конкурентности Swift подобный доступ может быть диагностирован как небезопасный. Но отсутствие диагностики, например в старом режиме проверки или при использовании небезопасных аннотаций, не означает, что гонка исчезла.
Изоляция actor распространяется на его хранимые свойства и изолированные методы. Она не распространяется автоматически на глобальные переменные, свойства других объектов или значения, возвращённые наружу.
Надёжный вариант — сделать actor владельцем состояния:
Теперь доступ к value проходит через один actor. Внешний код не получает прямой ссылки на изменяемое состояние и не может обойти его изоляцию обычным обращением к глобальной переменной.
Другой вариант — изолировать состояние глобальным actor, например @MainActor, если оно действительно должно использоваться только в этой изоляции. Это удобно для состояния пользовательского интерфейса, но не превращает код в универсально безопасный для произвольных фоновых исполнителей: переход к главному actor нужно явно учитывать.
Мьютекс подходит, когда состояние должно оставаться синхронным или используется низкоуровневый ресурс. Он требует вручную соблюдать протокол блокировок, избегать взаимных блокировок и не удерживать блокировку во время длительных операций. Actor обычно проще для асинхронного кода, но обращение к нему может приостанавливать задачу и требует учитывать его реентерабельность.
Важно отличать глобальную константу-ссылку от неизменяемого объекта. let запрещает переназначить ссылку, но не запрещает изменять объект, на который она указывает. Поэтому глобальная let сама по себе не делает внутреннее состояние ссылочного объекта потокобезопасным.
В серверном Swift-приложении несколько запросов обновляют глобальный кэш. Разработчик помещает часть операций в actor CacheManager, но оставляет глобальный словарь доступным для диагностического кода и фоновой очистки. В итоге основные запросы сериализованы, а очистка может одновременно менять тот же словарь.
Рассматривались три варианта. Оставить глобальный словарь без защиты проще всего, но это сохраняет гонку. Добавить мьютекс можно, однако все пути чтения и записи должны без исключения захватывать его, а длительные операции внутри критической секции ухудшат масштабируемость. Перенести словарь внутрь actor требует изменить интерфейс, зато делает единственную точку владения явной.
Выбран третий вариант: actor стал владельцем кэша, а очистка вызывается его методом. В результате все операции проходят через одну изоляцию, а наружу возвращаются копии или значения, не дающие обойти защиту. Это уменьшило риск скрытого конкурентного доступа ценой асинхронного интерфейса кэша.
let, чтобы устранить гонку?Нет. let защищает только саму ссылку или значение от переназначения. Если это ссылочный тип с изменяемыми свойствами, несколько задач всё ещё могут одновременно менять один объект. Безопасность появится только при неизменяемом содержимом либо при отдельной синхронизации доступа к объекту.
Обычный actor — это отдельный экземпляр-владелец состояния; доступ к нему выполняется через ссылку на этот экземпляр. Глобальный actor задаёт единую изоляцию, к которой можно привязать типы и функции, поэтому их состояние обслуживается одним глобальным исполнителем.
Глобальный actor удобен для логически единой области, например состояния интерфейса. Но чрезмерная привязка к одному глобальному actor может создать узкое место и усложнить тестирование. Обычный actor обычно лучше выражает самостоятельный ресурс с отдельным жизненным циклом.
Только при гарантии, что прямых обходных доступов действительно нет. Само объявление переменной не знает, что её следует использовать через конкретный actor, поэтому будущий код может нарушить это соглашение.
Надёжнее скрыть хранилище и предоставить только actor-методы. Если глобальный доступ необходим, следует явно привязать состояние к глобальному actor или использовать другой механизм синхронизации, а не полагаться на дисциплину команды.