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
- NSLock — Documentação para desenvolvedores da Apple.
- Equatable — Documentação para desenvolvedores da Apple.
- Sendable — Documentação para desenvolvedores da Apple.
- Concurrency — A linguagem de programação Swift, projeto Swift.
- Actors — SE-0306 — Swift Evolution, projeto Swift.
- Protect mutable state with Swift actors — Apple Developer, WWDC21.
- Install Swift — macOS — Swift.org.