Что такое actor в Swift concurrency и как он обеспечивает безопасность потоков
Что проверяют
Понимаете ли вы, как именно актор защищает состояние: не как лок в обёртке, а через изоляцию, которую проверяет компилятор. Главная ловушка вопроса это reentrancy, поэтому следом почти всегда спрашивают, почему внутри актора всё равно возможна гонка логики и что такое @MainActor.
Как отвечать
actor (Swift 5.5) это ссылочный тип, изменяемое состояние которого изолировано: обращаться к нему напрямую может только код, выполняющийся на самом акторе. Снаружи любой вызов изолированного метода или свойства требует await: вызов ставится в очередь последовательного исполнителя актора, и в каждый момент на акторе выполняется не больше одной задачи. Своего потока у актора нет, задачи работают на общем кооперативном пуле.
Изоляцию проверяет компилятор. Что стоит назвать:
Sendable: через границу актора передаются только значения, которые безопасно делить между задачами. В языковом режиме Swift 6 нарушения стали ошибками компиляции;nonisolated: методы иlet-свойства, которым изоляция не нужна, вызываются синхронно;- глобальные акторы:
@MainActorизолирует код на главном потоке, это штатный способ работать с UI; - reentrancy: на каждом
awaitвнутри метода актор отпускает исполнитель и может обслуживать другие вызовы. После возврата изawaitсостояние могло измениться.
Пример: кеш загрузки картинок. Проверили словарь, записи нет, вызвали await download(url), записали результат. Два одновременных запроса одного URL скачают картинку дважды: оба прошли проверку до первой записи. Решение: хранить в словаре не картинку, а Task, и второму вызывающему отдавать уже запущенную задачу.
Где спотыкаются
Говорят, что актор работает в своём потоке или что это «последовательная очередь с блокировкой». Потока нет, и актор не блокирует вызывающего, а приостанавливает его задачу.
Считают метод актора атомарным целиком. Атомарен только отрезок кода между двумя await.
Глушат ошибки строгой проверки через @unchecked Sendable или вешают @MainActor на всё подряд. В первом случае остаётся гонка данных, во втором лишняя работа на главном потоке и await там, где хватило бы struct или Mutex.