Как решить проблему reentrancy в акторах Swift concurrency?
Что проверяют
Понимаете ли вы, что актор в Swift защищает от гонок данных, но не от логических гонок: на каждом await внутри актора он может начать выполнять другой вызов. Следом обычно просят показать загрузку, которая не пойдёт в сеть дважды за одним и тем же ресурсом.
Как отвечать
Акторы в Swift реентерабельны. Код между двумя await выполняется без перерыва, но в точке приостановки актор освобождается и берёт следующее сообщение из своей очереди. Когда метод продолжится, состояние может быть уже другим. Классический баг: проверили кэш, не нашли, ушли в сеть через await, за это время второй вызов тоже не нашёл и тоже пошёл в сеть.
Нереентерабельных акторов в Swift нет, поэтому проблему решают дизайном:
- менять состояние синхронно: все проверки и изменения, которые должны быть атомарными, делают в одном отрезке без
await; - перепроверять после
await: любое предположение о состоянии, сделанное до приостановки, после неё считается устаревшим; - хранить задачу, а не результат: в словаре
[URL: Task<Data, Error>]лежит уже запущенная загрузка. Второй вызов находит её и делаетawait task.value, а не стартует новую. Запись в словарь идёт до первогоawait, поэтому место занято сразу; - выстраивать работу в очередь: если операции должны идти строго по одной, их передают через
AsyncStreamодному обработчику или связывают цепочкой, где каждая задача ждёт предыдущую.
Синхронный метод актора выполняется целиком, без чередования с другими вызовами. Это лишний повод выносить критичную логику в синхронные методы.
Где спотыкаются
Ставят флаг isUpdating и выходят через guard. Вызов, пришедший во время работы, просто теряется, а вызывающий код считает, что он выполнен. Это тихая потеря данных, а не синхронизация.
Считают, что @MainActor решает вопрос. Главный актор тоже реентерабелен: на await внутри метода @MainActor состояние экрана может поменяться точно так же.
Думают, что два последовательных await counter.increment() из одной задачи могут пересечься. Они выполняются по очереди, реентерабельность проявляется, когда вызовы идут из разных задач одновременно.