Метод actor приостановился на await: какое свойство его состояния нельзя считать сохранённым после возобновления?
После возобновления метода нельзя считать, что состояние actor осталось таким же, каким оно было до await. Пока задача приостановлена, actor может обработать другие изолированные вызовы, поэтому проверенное ранее условие может стать недействительным.
await не удерживает эксклюзивную блокировку actor. Он обозначает точку возможного переключения и повторного входа.
Actors и структурированная конкурентность появились в Swift, чтобы безопаснее работать с изменяемым общим состоянием без ручного управления потоками, очередями и блокировками. Подход переносит проверку изоляции на компилятор и делает асинхронные границы явными через async и await.
Исходная проблема состояла не только в одновременной записи из разных потоков. Даже последовательные асинхронные операции могли нарушать бизнес-инварианты, если между чтением состояния и его изменением выполнялся другой конкурентный код.
Предположим, метод actor проверяет баланс, затем ждёт сетевого подтверждения и после этого списывает деньги. Во время ожидания другой вызов может изменить баланс или выполнить такое же списание.
Если после await использовать старый результат проверки, возникнет логическая ошибка: actor по-прежнему защищён от одновременного доступа к памяти, но бизнес-операция уже не является атомарной. Это называют reentrancy — повторным входом в actor во время приостановки его метода.
Actor последовательно выполняет синхронные участки своей изолированной работы. Когда такой участок доходит до await, текущая задача приостанавливается, а actor может начать выполнять другую готовую задачу, обращающуюся к его состоянию.
Поэтому после await нужно заново читать критически важное состояние и повторно проверять условия. Если операция должна быть неделимой, асинхронную часть выносят за пределы изменения состояния, используют предварительное резервирование или моделируют процесс явной машиной состояний.
В примере вторая проверка обязательна: после первой проверки баланс мог измениться. Такой код защищает доступ к памяти, но не делает весь метод транзакцией относительно других вызовов.
Важно отличать безопасность данных от атомарности бизнес-операции. Actor предотвращает неконтролируемый одновременный доступ к изолированным данным, но разработчик сам должен определить, какие переходы состояния допустимы после приостановки.
В приложении два параллельных запроса пытались обновить токен авторизации. Actor хранил текущий токен, но метод обновления читал его, выполнял сетевой await, а затем записывал результат. При повторном входе более старый ответ мог перезаписать более новый токен.
Рассматривались три варианта:
async-кодом и повышала риск блокировки потока.Выбрали третий вариант. Он учитывал reentrancy, предотвращал дублирование сетевых запросов и позволял явно описать отмену и восстановление после ошибки. Actor отвечал за согласованность состояния, а сетевой вызов выполнялся без удержания несуществующей блокировки actor.
Нет. Actor обеспечивает изоляцию своего состояния, но не удерживает поток и не обязан блокировать все остальные вызовы на время await. Синхронный участок выполняется последовательно в рамках actor, однако приостановка освобождает actor для другой работы. Поэтому перенос кода с mutex на actor не гарантирует сохранение прежней атомарности.
Нет. Если задача получает значение из actor, затем выполняет await или обращается к actor повторно, между этими действиями состояние может измениться. Атомарность нужно предоставить одним изолированным методом, который выполняет весь неделимый переход без await, либо реализовать через резервирование и проверку версии.
Нет. Actor защищает только состояние, изолированное этим actor. Данные, доступные через nonisolated, небезопасные указатели, внешние mutable-объекты или несколько независимых actor, требуют отдельной стратегии синхронизации. Кроме того, actor не устраняет логические гонки: программа может быть безопасной на уровне памяти, но всё равно принять устаревшее решение после await.