Две задачи вызывают одну и ту же не изолированную async-функцию: что гарантирует Swift об одновременности её выполнения?
Swift не гарантирует последовательное выполнение вызовов одной и той же не изолированной async-функции. Две задачи могут выполнять её одновременно, поэтому доступ к общему изменяемому состоянию внутри функции должен быть защищён изоляцией: actor, глобальным актором, Mutex или другим корректным механизмом синхронизации.
Само наличие async означает возможность приостановки, но не последовательность, взаимное исключение и не привязку к одному потоку.
Модель async/await в Swift появилась как структурированный способ описывать приостанавливаемые операции вместо ручной работы с callback-цепочками и очередями GCD. Она разделяет две задачи: async описывает потенциальную приостановку, а tasks определяют конкурентное выполнение.
Для защиты состояния Swift предоставляет отдельные механизмы — actors, глобальные акторы и безопасные синхронные примитивы. Такое разделение не позволяет ошибочно считать любую асинхронную функцию автоматически сериализованной.
Если две конкурентные задачи вызывают одну функцию, её локальные значения обычно независимы: каждая задача получает собственный вызов и собственный стек выполнения. Но глобальное или разделяемое ссылочное состояние может изменяться одновременно.
Например, операция «прочитать значение, увеличить его и записать обратно» не становится атомарной только потому, что объявлена как async. Приостановка между шагами или параллельное выполнение двух вызовов может привести к гонке данных и потере обновления.
Неизолированная async-функция выполняется в контексте вызывающей задачи. После await она может продолжить работу позже и, как правило, не обязана использовать тот же поток. Если функцию вызывают две задачи, Swift не создаёт вокруг неё общий замок и не ставит вызовы в очередь.
Пример показывает допустимое перекрытие двух вызовов:
Обе задачи могут вывести start до того, как какая-либо из них выведет finish. Порядок вывода не следует считать фиксированным: планировщик выбирает, когда запускать и возобновлять задачи.
Если состояние принадлежит actor, доступ к его изолированным свойствам сериализуется исполнителем этого actor. Если состояние должно изменяться синхронно, без await, может подойти Mutex. Для неизменяемых значений безопасная передача обычно достигается через Sendable, но Sendable не превращает произвольный общий изменяемый объект в потокобезопасный.
Важно различать сериализацию вызовов и безопасность самой функции. Даже если конкретный вызывающий код сейчас запускает только одну задачу, отсутствие изоляции не даёт долгосрочной гарантии: будущая оптимизация или новый вызывающий путь может добавить параллельный запуск.
Сервис обновления токена хранит срок действия и сам токен в обычном ссылочном объекте. Два параллельных сетевых запроса видят истёкший срок и одновременно запускают обновление, после чего один результат может затереть другой.
Вариант с обычной async-функцией прост, но не защищает состояние. Вариант с ручной блокировкой обеспечивает взаимное исключение, однако требует аккуратно учитывать блокировку, возможные долгие операции и запрет удерживать синхронный замок через await.
Практичное решение — поместить состояние сервиса в actor, а сетевой вызов организовать так, чтобы состояние не удерживалось в виде блокировки во время ожидания. Это сериализует критические переходы состояния и сохраняет возможность конкурентного выполнения независимых операций. Цена решения — асинхронные обращения к actor и необходимость учитывать его реентерабельность в местах с await.
async-функция последовательность операций внутри одного вызова?Нет. Внутри одного вызова синхронный участок между точками приостановки выполняется последовательно, но после await выполнение может продолжиться значительно позже. Кроме того, сам вызов может быть частью конкурентной работы с другими вызовами этой функции.
async, чтобы устранить гонку при увеличении общего счётчика?Нет. async не предоставляет взаимного исключения и не делает составную операцию атомарной. Нужно изолировать счётчик actor-ом, защищать его подходящим синхронным примитивом либо изменить архитектуру так, чтобы состояние не разделялось.
Нет. Совпадение потоков в конкретном запуске не является контрактом Swift. Задачи могут приостанавливаться, возобновляться в другом месте и выполняться конкурентно; безопасность должна следовать из модели изоляции, а не из наблюдаемого поведения планировщика.