Требуется атомарно изменить общее состояние из синхронного кода без await: почему для этого выбирают Mutex, а не actor?
Для атомарного изменения состояния из синхронного кода подходит Mutex: он предоставляет синхронную критическую секцию, которую нельзя пересечь одновременно. Actor защищает состояние через асинхронную изоляцию, поэтому доступ к нему обычно требует await и может приостанавливаться.
Mutex уместен для коротких синхронных операций над небольшим общим состоянием. Он не делает защищаемый объект безопасным автоматически: каждый доступ к этому состоянию должен проходить через тот же mutex.
Традиционные многопоточные программы защищали общее изменяемое состояние блокировками. Такой подход позволяет синхронно выполнить последовательность операций, но требует вручную соблюдать дисциплину захвата и освобождения блокировки.
Actors были введены в модель Swift Concurrency для изоляции состояния на уровне типа и безопасного взаимодействия между асинхронными задачами. Они уменьшают число ручных блокировок, но меняют интерфейс доступа: изолированный метод actor вызывается асинхронно.
Представим синхронный метод, который должен проверить условие и тут же изменить счётчик. Если вместо единой критической секции выполнить чтение и запись раздельно, две конкурентные операции могут прочитать одно и то же старое значение и потерять одно из обновлений.
Actor не является заменой для любой блокировки. Его изолированный метод нельзя вызвать из обычного синхронного кода без перехода в асинхронный контекст, а разбиение операции через await может позволить другим вызовам actor вмешаться между шагами.
Mutex сериализует выполнение замыкания критической секции. Пока одна задача владеет блокировкой, другая ждёт освобождения mutex; проверка и изменение состояния внутри одной секции становятся неделимой для конкурирующих операций последовательностью.
Mutex защищает только данные, доступ к которым действительно проходит через него. Нельзя прочитать состояние напрямую в одном месте, а изменить через mutex в другом: такая схема снова допускает гонку.
Критическая секция должна быть короткой и синхронной. В неё не следует помещать await, сетевые операции или длительные вычисления: удержание блокировки на время ожидания ухудшает масштабируемость и может привести к блокировкам между компонентами.
Actor предпочтительнее, когда состояние естественно обслуживается асинхронными сообщениями, операция включает ожидание или объект должен сам последовательно обрабатывать поток запросов. Mutex предпочтительнее для маленького состояния и короткой атомарной операции, особенно когда вызывающий API остаётся синхронным.
Синхронный кэш должен увеличивать число обращений к ключу. Вариант с actor хорошо изолирует словарь, но превращает увеличение счётчика в асинхронный вызов и усложняет использование кэша из синхронного участка. Вариант с общей переменной без синхронизации прост, но приводит к гонке данных.
Можно было бы применить Dispatch-очередь или ручную блокировку. Очередь добавляет отдельную модель планирования, а ручная блокировка требует особенно осторожно управлять временем жизни блокировки. Выбран Mutex, потому что операция короткая, синхронная и должна быть атомарной непосредственно в месте изменения.
В результате каждый инкремент выполняется последовательно, а внешний API остаётся синхронным. Если позже операция начнёт обращаться к базе данных или ожидать сеть, состояние лучше разделить: короткие счётчики оставить под mutex, а долгую асинхронную координацию передать actor.
1. Делает ли Mutex весь содержащий его класс Sendable автоматически?
Нет. Mutex защищает конкретное состояние, но разработчик должен убедиться, что все изменяемые поля класса либо защищены синхронизацией, либо сами безопасны для передачи. Наличие одного mutex не покрывает другие поля и внешние ссылки.
2. Можно ли удерживать Mutex во время await?
Практически это следует считать недопустимой архитектурой. Критическая секция mutex синхронная, а await передаёт управление и может надолго оставить ресурс занятым; другие задачи будут ждать, а при зависимостях между операциями можно получить взаимную блокировку. Нужно сначала захватить и быстро обновить состояние, затем выполнять асинхронную работу уже после освобождения mutex.
3. Гарантирует ли Mutex атомарность нескольких вызовов его методов?
Нет, он гарантирует атомарность только каждой отдельной критической секции. Если сначала отдельно проверить значение, а затем отдельным вызовом изменить его, другая задача может вмешаться между этими вызовами. Проверку и изменение нужно помещать в одну секцию withLock или проектировать операцию на уровне actor как единое изолированное действие.