Чтобы сохранить атомарное сравнение-и-замену при переносе модели Swift в актор, оставьте сравнение и присваивание внутри одного синхронного метода, изолированного актором. Вызывающие используют await, но сам метод не должен приостанавливаться между проверкой и заменой.

Это руководство предназначено для разработчика Mac, переносящего небольшую настольную модель, ранее защищаемую блокировкой. Конкретная задача — заменить синтетическую метку формы только в том случае, если её текущее значение всё ещё соответствует ожидаемому значению. Пример работает полностью в памяти и не использует сетевые запросы, внешние записи или сохранение.

Определите контракт значения перед изменением модели

Для этого примера compareAndReplace(expected:replacement:) имеет три правила: сравнить текущую метку с expected; при равенстве назначить replacement и вернуть true; иначе вернуть false без изменения метки. Сравнение использует равенство значений Swift. Документация стандартной библиотеки Equatable определяет это сравнение через == и различает равенство значений и идентичность экземпляров класса.

Сравнение должно использовать значение, актуальное в момент выполнения операции. Более ранний снимок может предоставить ожидаемое значение, но он не может установить, что замена должна выполниться позже. Это решение принадлежит операции, владеющей мутацией.

Текущее значение Ожидаемое значение Замена Результат Значение после
"Circle" "Circle" "Square" true "Square"
"Square" "Circle" "Triangle" false "Square"
"Square" "Square" "Square" true "Square"

Последняя строка намеренна: успех означает, что сравнение совпало и присваивание было принято. Это не означает, что видимое значение изменилось. Изменение этого соглашения о возврате при миграции изменило бы контракт.

В реализации на основе блокировки всё сравнение и присваивание должны находиться в одной защищённой критической секции. Apple документирует NSLock Foundation как способ защитить критическую секцию, чтобы она выполнялась атомарно. Для миграции на актор сохраните эту границу операции: перенесите всё решение внутрь актора, а не распределяйте его части среди вызывающих.

Здесь «атомарный» означает, что другая операция на этом акторе не может перемежаться между сравнением и присваиванием. Этот пример не делает утверждений об аппаратных атомарных инструкциях или бесблокировочном выполнении.

Почему await вызывающего не нарушает операцию

Синхронный метод актора может требовать await при вызове извне его домена изоляции. Вызывающий может потребоваться приостановиться в ожидании входа в актор. Как только метод начинает выполняться, его обычное синхронное тело не имеет точки приостановки, в которой другая задача могла бы войти и изменить состояние того же актора. Swift документирует это различие в SE-0306: Actors.

Руководство по языку рекомендует инкапсулировать обновления в синхронных методах актора, потому что эти методы не могут содержать потенциальных точек приостановки. Сессия Apple по акторам аналогично объясняет, что синхронный код актора завершается без прерывания другой задачей его изолированного выполнения актором. Это гарантии, используемые для получения данной реализации. См. Concurrency и Protect mutable state with Swift actors.

Полезной границей является, таким образом, один вызов актора, содержащий как условие, так и его результирующую мутацию. Вызывающий может сначала прочитать снимок, выполнить локальный расчёт, а затем отправить условную замену. Актор должен снова проверить условие при обработке этой заявки.

Что идет не так с отдельными чтениями и записями?

Предположим, миграция предоставляет отдельные методы снимка и безусловной замены. Один вызывающий читает "Circle". Другой вызывающий меняет метку на "Square". Затем первый вызывающий записывает "Triangle" на основе своего раннего чтения. Каждый отдельный метод может уважать изоляцию актора, но общая последовательность не выполнит предполагаемый контракт сравнения-и-замены.

Этот иллюстративный график объясняет дефект, не завися от конкретного планировщика задач. Изоляция актора защищает каждую операцию; программисту всё ещё нужно выбрать операцию достаточно большую, чтобы сохранить условие приложения.

Создайте полный локальный пример на вашем Mac

Используйте установленный инструментарий Swift 6. Официальное руководство по установке на macOS Swift описывает установку через Swiftly и разработку с Xcode. В Терминале проверьте выбранный компилятор перед продолжением:

swiftc --version

Сохраните следующую полную программу как CompareReplace.swift в рабочей папке. Она использует только стандартную библиотеку Swift, которая доступна без явного импорта. Файл определяет актор, его начальное состояние, все примеры входных данных и асинхронную точку входа.

actor ShapeLabelModel {
    private var label: String

    init(initial: String) {
        label = initial
    }

    func snapshot() -> String {
        label
    }

    func compareAndReplace(
        expected: String,
        replacement: String
    ) -> Bool {
        guard label == expected else {
            return false
        }
        label = replacement
        return true
    }
}

@main
struct CompareReplaceDemo {
    static func main() async {
        let model = ShapeLabelModel(initial: "Circle")

        let matched = await model.compareAndReplace(
            expected: "Circle", replacement: "Square"
        )
        let afterMatch = await model.snapshot()
        precondition(matched && afterMatch == "Square")

        let stale = await model.compareAndReplace(
            expected: "Circle", replacement: "Triangle"
        )
        let afterStale = await model.snapshot()
        precondition(!stale && afterStale == "Square")

        let unchanged = await model.compareAndReplace(
            expected: "Square", replacement: "Square"
        )
        precondition(unchanged)

        let contested = ShapeLabelModel(initial: "Circle")
        async let first = contested.compareAndReplace(
            expected: "Circle", replacement: "Square"
        )
        async let second = contested.compareAndReplace(
            expected: "Circle", replacement: "Triangle"
        )
        let (firstWon, secondWon) = await (first, second)
        let finalLabel = await contested.snapshot()

        precondition(firstWon != secondWon)
        precondition(
            finalLabel == (firstWon ? "Square" : "Triangle")
        )

        let cycle = ShapeLabelModel(initial: "Circle")
        let oldSnapshot = await cycle.snapshot()
        let movedAway = await cycle.compareAndReplace(
            expected: "Circle", replacement: "Square"
        )
        let movedBack = await cycle.compareAndReplace(
            expected: "Square", replacement: "Circle"
        )
        let matchedAgain = await cycle.compareAndReplace(
            expected: oldSnapshot, replacement: "Triangle"
        )
        let afterCycle = await cycle.snapshot()
        precondition(movedAway && movedBack && matchedAgain)
        precondition(afterCycle == "Triangle")

        print("Matching replacement: \(matched)")
        print("Stale replacement: \(stale)")
        print("Equal replacement: \(unchanged)")
        print("Exactly one competing replacement succeeded.")
        print("A restored value matches the old snapshot.")
    }
}

Скомпилируйте и запустите её из этой папки. Опция -parse-as-library учитывает явную точку входа @main, в то время как -swift-version 6 выбирает режим языка Swift 6.

swiftc -swift-version 6 -parse-as-library CompareReplace.swift -o compare-demo
./compare-demo

Ожидаемый вывод, полученный из проверок программы, таков:

Matching replacement: true
Stale replacement: false
Equal replacement: true
Exactly one competing replacement succeeded.
A restored value matches the old snapshot.

Код был проверен по документации, но не был выполнен здесь, так как в этой среде нет компилятора Swift. Включённые предусловия обеспечивают локальные проверки при запуске; они не являются утверждением о завершённом выполнении.

Что устанавливают конкурирующие вызовы

Две декларации async let создают дочерние задачи без ожидания первого результата перед запуском второй. Это использование структурированного параллелизма следует объяснению конкурентных асинхронных вызовов в руководстве по языку. Оба вызывающих ожидают "Circle", и каждый предлагает разную замену.

Для этой конкретной настройки ровно один успешно завершается: тот, который выполняется первым, меняет значение, отличное от "Circle"; другой затем не проходит сравнение. Тест допускает любого победителя. Swift не гарантирует, что задачи, ожидающие актора, выполняются в том порядке, в котором они изначально его ожидали, как описано в SE-0306.

Пример также разделяет ожидаемые вызовы от выражений precondition. Сначала он сохраняет каждый результат в локальную константу, а затем проверяет эту константу. Это удерживает асинхронную работу вне синхронных выражений утверждений.

Держите асинхронную подготовку вне окончательного сравнения

Будущая версия может вычислить предлагаемую метку асинхронно, используя другую синтетическую локальную работу. Завершите эту подготовку перед вызовом синхронного метода сравнения-и-замены. Передайте исходное ожидаемое значение и вычисленную замену вместе; обрабатывайте false как отклонённую попытку.

Метод актора, который проверяет равенство, ожидает подготовку, а затем безусловно присваивает, имеет разрыв. Во время его приостановки другая задача может изменить состояние актора. Возобновлённый метод будет полагаться на условие, которое может больше не выполняться. Документированная Swift модель реентрантности актора явно допускает такое перемежающееся выполнение.

Если подготовка находится в асинхронном методе актора, вызовите синхронный метод сравнения после возврата подготовки. Этот окончательный метод должен выполнить свежую проверку. Проверка, выполненная до приостановки, может помочь избежать лишней работы, но не может авторизовать окончательное присваивание.

При несовпадении сохраните предложенный результат, если настольному интерфейсу нужно его показать, получите свежий снимок и решите, пересчитывать ли или сообщать о конфликте. Автоматическая замена ожидаемого значения и повторная попытка изменили бы запрос с «замени значение, которое я наблюдал» на «продолжай пытаться против более новых значений». Сделайте это поведение явным продуктовым решением.

Не переносите старую блокировку Foundation через await. Apple требует, чтобы NSLock разблокировался в том же потоке, который его заблокировал. Показанный здесь дизайн актора достигает нужного результата через короткую синхронную изолированную операцию, не удерживая блокировку через асинхронную работу.

Сохраняйте семантику равенства и признавайте их пределы

Проверка цикла демонстрирует тонкую границу. Вызывающий делает снимок "Circle"; модель меняется на "Square", затем обратно на "Circle". Сравнение со старым снимком успешно, потому что контракт сравнивает текущее значение, а не последовательность изменений, приведших к нему.

Если ваше фактическое требование — «заменять только если с момента моего снимка не произошло ни одной принятой мутации», то сравнения значений недостаточно. Поддерживаемым дизайном является возврат снимка, содержащего и значение, и токен ревизии, затем проверка токена и обновление значения вместе внутри одного синхронного метода актора. Это более сильный, другой контракт. Определите поведение продвижения и исчерпания токена перед реализацией; не вводите его молча при миграции, сохраняющей значения.

Для более крупных моделей используйте значение снимка, чье равенство охватывает каждое поле, релевантное для замены. Swift может синтезировать Equatable для структуры, когда все хранимые свойства соответствуют, согласно справке по Equatable. Реализация равенства, игнорирующая релевантное поле, позволит замены несмотря на изменения в этом поле.

Храните значения, пересекающие границу актора, подходящими для конкурентного использования. Документация по Sendable объясняет, что отправляемые значения могут передаваться между доменами параллелизма и что отправляемые структуры должны содержать отправляемые члены. Оборачивание произвольной изменяемой ссылки в структуру не устанавливает это свойство.

Для этой локальной модели миграция завершена, когда актор владеет изменяемой меткой, каждая условная замена входит через объединённую операцию, а вызывающие интерпретируют её булев результат согласно исходному контракту. Держите окончательное сравнение и присваивание вместе, затем позвольте окружающему рабочему процессу рабочего стола решить, что означает отклонённая попытка.

Источники