За счёт чего actor предотвращает одновременное выполнение изолированных участков на разных задачах?

За счёт чего actor предотвращает одновременное выполнение изолированных участков на разных задачах?

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

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

Actor предоставляет сериализованный доступ к своему изолированному состоянию: в каждый момент времени выполняется только один изолированный фрагмент работы этого actor. Это не означает привязку к одному потоку: разные фрагменты могут выполняться на разных потоках, но не одновременно.

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

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

Actors перенесли правило владения состоянием на уровень модели конкурентности Swift. Компилятор проверяет, что изолированное состояние доступно только через контролируемые границы actor, а среда выполнения сериализует такие обращения.

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

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

Actor устраняет одновременный доступ к своему изолированному состоянию, но не делает любую последовательность операций автоматически атомарной. Особенно важно учитывать точки await: во время приостановки actor может начать выполнять другой изолированный вызов.

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

У actor есть изолированное состояние и исполнитель, который обрабатывает обращения к этому состоянию по одному. Когда задача вызывает изолированный метод из другого контекста, она передаёт работу границе actor; после этого компилятор требует явно обозначить потенциальную приостановку через await.

Сериализация относится к изолированным участкам, а не к физическому потоку. После приостановки выполнение может возобновиться на другом потоке, поэтому полагаться на thread affinity нельзя.

actor Counter { private var value = 0 func increment() { value += 1 } func read() -> Int { value } } let counter = Counter() await counter.increment() let value = await counter.read()

В примере два вызова не изменяют value одновременно. При этом Counter не обещает, что increment всегда выполняется на одном и том же системном потоке.

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

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

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

Сервис кэширования хранит словарь результатов и при отсутствии значения обращается к сети. Вариант с обычным словарём и блокировкой защищает отдельные обращения, но сложная логика загрузки легко приводит к ошибкам при ручном управлении блокировкой. Вариант с отдельной serial queue работает, однако изоляция остаётся соглашением API и хуже проверяется компилятором.

Actor делает владение словарём явным и сериализует его чтение и изменение. Практическое решение — не удерживать блокировку во время сетевого ожидания и отдельно продумать состояние загрузки: например, хранить признак выполняющегося запроса, чтобы параллельные вызовы не запускали дубликаты.

Результат: гонки доступа к словарю устраняются, но логика дедупликации запросов всё равно проектируется явно. Actor защищает состояние, а не автоматически оптимизирует бизнес-операцию.

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

  1. Actor всегда выполняется на одном потоке?

Нет. Actor обеспечивает изоляцию и последовательное выполнение изолированных участков, но не закрепляет их за конкретным потоком. Планировщик может переместить продолжение задачи между потоками, особенно после await.

  1. Могут ли два вызова методов одного actor выполняться одновременно?

Два непрерывных изолированных участка не выполняются одновременно. Однако метод может приостановиться на await, после чего другой вызов получит возможность выполнить свой участок; затем первый метод продолжится. Поэтому actor является реентерабельным, и состояние после await нельзя считать неизменным без повторной проверки.

  1. Достаточно ли actor для атомарного перевода средств между двумя actor?

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