Сохраняет ли actor атомарность составной операции, если её реализация содержит await?
Нет. Actor сериализует доступ к своему изолированному состоянию только до точки приостановки и после возобновления, но не удерживает эксклюзивное выполнение всей функции через await.
Во время приостановки другой вызов actor может изменить его состояние. Поэтому проверка условия до await и изменение состояния после await не образуют одну атомарную операцию.
Actors и async/await появились в Swift Concurrency, чтобы безопаснее организовать доступ к изменяемому состоянию и явно обозначить места, где выполнение может приостановиться. Это решает проблему обычных гонок данных между потоками, но не гарантирует сохранение бизнес-инварианта на протяжении всей асинхронной функции.
Актор предоставляет изоляцию состояния, а не транзакционность. Такой дизайн позволяет не блокировать actor на время долгой асинхронной операции и повышает отзывчивость системы.
Рассмотрим операцию списания средств: сначала проверяется баланс, затем выполняется асинхронная проверка, после чего сумма списывается. Если между проверкой и списанием actor обслужит другой вызов, оба вызова могут увидеть один и тот же исходный баланс.
Это не обязательно гонка данных: отдельные обращения к свойству остаются защищёнными actor. Ошибка состоит в том, что логическая операция, включающая несколько участков, перестаёт быть атомарной, и инвариант баланса может нарушиться.
У actor есть последовательные участки выполнения, разделённые точками await. Пока метод исполняется без приостановки, другой изолированный вызов не выполняется одновременно с ним. Но на await actor становится доступен для обработки других сообщений.
Два вызова spend(80) могут оба пройти проверку баланса, затем приостановиться и последовательно выполнить списание. Итоговый баланс станет отрицательным, хотя каждое отдельное чтение и изменение выполнялось изолированно.
Безопасные варианты зависят от требований операции:
await между проверкой и изменением, если это возможно;доступно, зарезервировано, завершено, отменено;Нельзя решать проблему простым добавлением ещё одного actor: если операция всё равно разбита await, другой вызов сможет попасть между её частями. Также не следует рассчитывать на порядок вызовов из разных задач как на гарантию атомарности.
В приложении оформлялся заказ. Actor хранил остаток товара, а метод сначала проверял наличие, затем ожидал ответ платёжного сервиса и после этого уменьшал остаток. При параллельных заказах одного товара оба запроса иногда проходили проверку.
Рассматривались два варианта. Первый — удерживать блокировку на время платежа; он плохо сочетается с моделью actor и может надолго задерживать другие операции. Второй — сразу переводить товар из состояния доступен в зарезервирован, затем проводить оплату и при ошибке возвращать резерв.
Выбрали второй вариант. Критическая проверка и резервирование выполнялись одним непрерывным изолированным участком, а внешняя операция оплаты происходила уже после него. Это сохранило инвариант остатка и позволило actor обслуживать другие запросы во время ожидания платежа.
Является ли любой код внутри метода actor атомарным, если в нём нет явного await?
Да, в отношении других изолированных вызовов actor такой участок выполняется без добровольной приостановки метода. Однако синхронный код может вызвать косвенную блокировку потока, бесконечный цикл или взаимодействие с незащищенным общим состоянием — actor не устраняет эти проблемы.
Достаточно ли прочитать состояние actor до await, сохранить его в локальной переменной и использовать после возобновления?
Нет. Локальная переменная сохранит старое значение, но само состояние actor за время приостановки могло измениться. Такое значение можно использовать только как снимок, если бизнес-логика допускает устаревшие данные; для принятия решения обычно нужна повторная проверка или атомарное резервирование.
Можно ли сделать всю операцию nonisolated, чтобы избежать прерывания actor?
Нет. nonisolated убирает требование выполнять метод на actor, но не делает доступ к изменяемому состоянию безопасным и не превращает операцию в атомарную. Такой метод не должен напрямую обращаться к изолированному изменяемому состоянию actor; правильнее явно спроектировать короткий изолированный участок и вынести долгую внешнюю работу за его пределы.