Para preservar a comparação e substituição atômicas ao migrar um modelo Swift para um ator, mantenha a comparação e a atribuição dentro de um único método síncrono isolado pelo ator. Os chamadores usam await, mas o próprio método não deve ser suspenso entre a verificação e a substituição.

Este passo a passo é para um desenvolvedor de Mac que está migrando um pequeno modelo de desktop antes protegido por um bloqueio. A tarefa concreta é substituir um rótulo sintético de forma somente se o valor atual ainda corresponder ao valor esperado. O exemplo opera inteiramente na memória e não usa solicitações de rede, registros externos nem persistência.

Defina o contrato do valor antes de alterar o modelo

Neste exemplo, compareAndReplace(expected:replacement:) tem três regras: comparar o rótulo atual com expected; se forem iguais, atribuir replacement e retornar true; caso contrário, retornar false sem alterar o rótulo. A comparação usa a igualdade de valores do Swift. A documentação da biblioteca padrão sobre Equatable define essa comparação por meio de == e distingue a igualdade de valores da identidade de instâncias de classe.

A comparação deve usar o valor atual no momento em que a operação é executada. Um instantâneo anterior pode fornecer o valor esperado, mas não pode determinar que a substituição deva ser bem-sucedida mais tarde. Essa decisão cabe à operação que controla a mutação.

Valor atual Valor esperado Substituição Resultado Valor posterior
"Circle" "Circle" "Square" true "Square"
"Square" "Circle" "Triangle" false "Square"
"Square" "Square" "Square" true "Square"

A última linha é intencional: sucesso significa que a comparação correspondeu e a atribuição foi aceita. Não significa que o valor visível mudou. Alterar essa convenção de retorno durante a migração mudaria o contrato.

Em uma implementação baseada em bloqueio, toda a comparação e a atribuição devem ocorrer na mesma seção crítica protegida. A Apple documenta o NSLock do Foundation como uma forma de proteger uma seção crítica para que ela seja executada atomicamente. Na migração para um ator, preserve esse limite da operação: mova toda a decisão para o ator, em vez de distribuir suas partes entre os chamadores.

Aqui, “atômico” significa que nenhuma outra operação neste ator pode se intercalar entre a comparação e a atribuição. Este exemplo não faz nenhuma afirmação sobre instruções atômicas de hardware ou execução sem bloqueios.

Por que o await do chamador não interrompe a operação

Um método síncrono de ator pode exigir await quando chamado de fora do domínio de isolamento do ator. O chamador talvez precise ser suspenso enquanto aguarda para entrar no ator. Quando o método é executado, seu corpo síncrono comum não tem um ponto de suspensão no qual outra tarefa possa entrar e alterar o estado do mesmo ator. O Swift documenta essa distinção em SE-0306: Actors.

O guia da linguagem recomenda encapsular atualizações em métodos síncronos de ator, pois esses métodos não podem conter possíveis pontos de suspensão. A sessão da Apple sobre atores também explica que o código síncrono é concluído sem que outra tarefa interrompa sua execução isolada pelo ator. Essas são as garantias usadas para derivar esta implementação. Consulte Concurrency e Protect mutable state with Swift actors.

O limite útil é, portanto, uma única chamada ao ator que contenha tanto a condição quanto a mutação resultante. Um chamador pode primeiro ler um instantâneo, fazer um cálculo local e então enviar uma substituição condicional. O ator deve verificar novamente a condição ao processar essa solicitação.

O que dá errado com leituras e gravações separadas?

Suponha que uma migração exponha métodos separados para obter um instantâneo e fazer uma substituição incondicional. Um chamador lê "Circle". Outro chamador altera o rótulo para "Square". Então o primeiro chamador grava "Triangle" com base na leitura anterior. Cada método individual poderia respeitar o isolamento do ator, mas a sequência completa não atenderia ao contrato pretendido de comparação e substituição.

Esta sequência ilustrativa explica o defeito sem depender de um agendador de tarefas específico. O isolamento do ator protege cada operação; ainda cabe ao programador escolher uma operação suficientemente abrangente para preservar a condição da aplicação.

Crie um exemplo local completo no seu Mac

Use um toolchain Swift 6 instalado. O guia oficial do Swift para instalação no macOS descreve a instalação por meio do Swiftly e o desenvolvimento com o Xcode. No Terminal, confira o compilador selecionado antes de continuar:

swiftc --version

Salve o programa completo a seguir como CompareReplace.swift em uma pasta de trabalho. Ele usa apenas a biblioteca padrão do Swift, disponível sem uma importação explícita. O arquivo define o ator, seu estado inicial, todas as entradas de exemplo e um ponto de entrada assíncrono.

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.")
    }
}

Compile e execute o programa nessa pasta. A opção -parse-as-library comporta o ponto de entrada explícito @main, enquanto -swift-version 6 seleciona o modo de linguagem Swift 6.

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

A saída esperada, derivada das verificações do programa, é:

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

O código foi conferido com base na documentação, mas não foi executado aqui porque este ambiente não tem um compilador Swift. As pré-condições incluídas oferecem verificações locais quando você o executar; elas não significam que a execução tenha sido concluída.

O que as chamadas concorrentes demonstram

As duas declarações async let criam tarefas-filhas sem esperar pelo primeiro resultado antes de iniciar a segunda. Esse uso da concorrência estruturada segue a explicação das chamadas assíncronas concorrentes no guia da linguagem. Ambos os chamadores esperam "Circle", e cada um propõe uma substituição diferente.

Nesta configuração específica, exatamente uma operação tem sucesso: a que for executada primeiro altera o valor para algo diferente de "Circle"; em seguida, a outra falha na comparação. O teste permite qualquer uma das operações vencer. O Swift não garante que as tarefas que aguardam um ator sejam executadas na ordem em que começaram a aguardá-lo, conforme documentado em SE-0306.

O exemplo também separa chamadas com await de expressões precondition. Primeiro, ele armazena cada resultado em uma constante local e, em seguida, verifica essa constante. Assim, o trabalho assíncrono fica fora das expressões síncronas de asserção.

Mantenha a preparação assíncrona fora da comparação final

Uma versão futura poderia calcular um rótulo proposto de forma assíncrona, usando outro trabalho local sintético. Conclua essa preparação antes de chamar o método síncrono de comparação e substituição. Passe juntos o valor esperado original e a substituição calculada; trate false como uma tentativa rejeitada.

Um método de ator que verifica a igualdade, aguarda a preparação e depois atribui incondicionalmente tem uma lacuna. Durante a suspensão, outra tarefa pode alterar o estado do ator. Ao retomar, o método dependeria de uma condição que talvez já não seja válida. O modelo de reentrância dos atores documentado pelo Swift permite explicitamente essa intercalação.

Se a preparação estiver em um método assíncrono do ator, chame o método síncrono de comparação quando a preparação terminar. Esse método final deve fazer uma nova comparação. Uma verificação feita antes da suspensão pode ajudar a evitar trabalho desnecessário, mas não pode autorizar a atribuição final.

Se houver incompatibilidade, mantenha o resultado proposto caso a interface de desktop precise exibi-lo, obtenha um instantâneo atualizado e decida se recalcula o valor ou comunica o conflito. Substituir automaticamente o valor esperado e tentar novamente mudaria a solicitação de “substitua o valor que observei” para “continue tentando com valores mais recentes”. Transforme esse comportamento em uma decisão explícita de produto.

Não mantenha o antigo bloqueio do Foundation durante um await. A Apple exige que um NSLock seja desbloqueado na mesma thread que o bloqueou. O projeto de ator mostrado aqui alcança o resultado necessário por meio de uma operação síncrona isolada e breve, sem manter um bloqueio durante o trabalho assíncrono.

Preserve a semântica da igualdade e reconheça seus limites

A verificação do ciclo demonstra um limite sutil. Um chamador obtém um instantâneo de "Circle"; o modelo muda para "Square" e depois volta para "Circle". A comparação com o instantâneo antigo tem sucesso porque o contrato compara o valor atual, e não a sequência de alterações que levou a ele.

Se o requisito real for “substituir somente se nenhuma mutação aceita tiver ocorrido desde meu instantâneo”, comparar apenas o valor não basta. Uma solução compatível é retornar um instantâneo que contenha tanto um valor quanto um token de revisão e, então, verificar o token e atualizar o valor juntos dentro de um único método síncrono do ator. Esse é um contrato diferente e mais forte. Defina como o token avança e como lidar com seu esgotamento antes de implementar essa solução; não a introduza silenciosamente em uma migração que preserva valores.

Para modelos maiores, use um valor de instantâneo cuja igualdade abranja todos os campos relevantes para a substituição. O Swift pode sintetizar Equatable para uma estrutura quando todas as propriedades armazenadas estão em conformidade, segundo a referência de Equatable. Uma implementação de igualdade que ignore um campo relevante permitiria substituições apesar de alterações nesse campo.

Mantenha os valores que atravessam o limite do ator adequados ao uso concorrente. A documentação de Sendable explica que valores Sendable podem passar entre domínios de concorrência e que estruturas Sendable devem conter membros Sendable. Envolver uma referência mutável arbitrária em uma estrutura não estabelece essa propriedade.

Para este modelo local, a migração está concluída quando o ator controla o rótulo mutável, cada substituição condicional passa pela operação combinada e os chamadores interpretam o resultado Booleano de acordo com o contrato original. Mantenha a comparação final e a atribuição juntas e deixe o fluxo de trabalho de desktop ao redor decidir o que significa uma tentativa rejeitada.

Fontes