Swift e Objective-C

[Swift avançado #3] Migração de Sendable e dos erros de concorrência do Swift 6

Ao ativar o modo Swift 6, erros de concorrência surgem por toda parte, com Sendable no centro. Este artigo explica o significado desse protocolo e se valores podem atravessar limites de isolamento, organizando a migração em: transformar em struct, tornar imutável ou promover a actor.

7 min de leitura
Imagem de capa de [Swift avançado #3] Migração de Sendable e dos erros de concorrência do Swift 6

Se sua equipe ativou o modo Swift 6, provavelmente se lembra do momento: um projeto que funcionava normalmente passa a exibir dezenas ou centenas de erros de concorrência.

O protagonista da maioria das mensagens de erro é um só: Sendable.

A terceira parte da série Concurrency organiza esta última peça do quebra-cabeça e a strict concurrency do Swift 6.

A pergunta de Sendable — este valor pode atravessar o limite?

No artigo sobre actor, definimos isolamento como um “contexto de execução serial”: dentro de um actor, sobre o MainActor e em um pool cooperativo que não pertence a nenhum dos dois.

Mas os valores atravessam esses limites o tempo todo. Eles entram como argumentos de métodos de actor, saem como valores de retorno e são capturados por closures de Task.

É aí que surge o risco. O isolamento protege o estado do próprio actor, mas e se o valor que atravessa o limite for um tipo por referência mutável?

A mesma instância pode ficar nas mãos de dois isolamentos ao mesmo tempo, e a condição de corrida de dados que o actor bloqueava volta por meio do objeto trazido. É como ter uma fiscalização de fronteira rigorosa sem verificar as mercadorias que entram.

Sendable define esse critério de inspeção. É um protocolo marcador, sem métodos obrigatórios, e seu significado é simples:

Valores desse tipo podem ser usados simultaneamente com segurança depois de atravessar um limite de isolamento.

Quais tipos são seguros? A intuição é direta.

Tipos por valor são copiados ao atravessar o limite, então são seguros se todas as propriedades armazenadas forem Sendable. Int, String e structs e enums compostos apenas por propriedades Sendable entram aqui. Na maioria dos casos, o compilador os reconhece automaticamente.

Actors também são seguros, pois a autoproteção já vem integrada.

Classes imutáveis (final e apenas com propriedades let) também são seguras. Se nada pode mudar, não há corrida.

Há exatamente um perigo: classes com estado mutável. A fórmula do artigo sobre priorizar tipos por valor, “compartilhado + mutável = perigoso”, é o próprio critério de Sendable.

Tipos de função têm uma anotação @Sendable específica. A closure de Task { } é uma closure @Sendable típica, e uma closure com essa marca não pode capturar valores non-Sendable.

Como vimos no artigo sobre closures, capturas podem virar rotas de contrabando através do isolamento; por isso a linguagem verifica a própria rota.

strict concurrency — De avisos a erros, de disciplina a verificação

Essas regras já existiam antes do Swift 6, mas ficavam silenciosas por padrão.

O ponto central do modo de linguagem Swift 6 é ativar todas essas verificações e promovê-las a erros: strict concurrency. Com complete checking, todo ponto em que um valor non-Sendable atravessa um limite de isolamento vira erro de compilação.

Os erros não aparecem porque o código ficou pior de repente. Apenas os potenciais data races que já existiam agora ficaram visíveis.

É como quando os optionals foram introduzidos e todos os lugares onde um valor podia ser nil vieram à tona. Assim como os nil checks naquela época, agora as suposições sobre concorrência migram para o sistema de tipos.

Felizmente, o compilador também está ficando mais inteligente. O Swift 6 inclui a análise de isolamento baseada em regiões (proposta Swift Evolution SE-0414, region-based isolation).

Até valores non-Sendable podem ser movidos quando se prova que “quem os enviou não vai tocá-los novamente”.

Um valor cuja propriedade foi transferida não pode criar uma corrida. Por isso, muito código que teoricamente deveria falhar na prática passa.

A anotação sending em parâmetros segue a mesma direção. Em resumo, a regra está ficando mais precisa: de “sempre proibido” para “permitido quando a segurança é comprovada”.

Diagrama de verificação: struct, actor e classes final com let passam; classes mutáveis são rejeitadas
O único perigo são classes com estado mutável

Migração na prática — Soluções por tipo de erro

A maioria dos erros se resume a alguns padrões. Estas são as soluções padrão para cada tipo.

Tipo 1. Meu modelo é non-Sendable. É o erro mais comum e mais saudável. A primeira solução é transformá-lo em struct.

Se um modelo de dados que não precisa de identidade de referência foi declarado como class, esta é a oportunidade de migrá-lo para um tipo por valor.

Se ele precisa ser class, torne-o imutável com final + let e adote Sendable. Se precisa ser mutável, isso significa que o estado precisa de um proprietário; considere promovê-lo a actor.

Tipo 2. Variáveis globais e static var. Todos os locais que alertávamos como “static var é, na prática, estado global” viram erros.

Se você realmente precisa de estado global, declare o isolamento. Adicione @MainActor para o que envolve UI; caso contrário, envolva em um actor ou torne-o imutável com let.

Tipo 3. Uma classe de delegate ou callback fica presa no limite. Isso aparece com frequência nos pontos que encontram APIs da era do UIKit.

Se o tipo é essencialmente exclusivo da thread principal, declarar @MainActor costuma ser a resposta. Você transforma em declaração explícita o fato implícito de que “essa classe sempre foi usada na thread principal”.

Tipo 4. É realmente seguro, mas o compilador não sabe. Por exemplo, classes protegidas internamente por locks e wrappers de bibliotecas C.

A saída é @unchecked Sendable. Ela declara “eu garanto que é seguro, então desative a verificação”, mas unchecked é uma ferramenta da família unsafe, como o nome indica.

Seguindo o princípio da saída explícita do primeiro artigo de Filosofia, deixe em comentário a base da garantia (qual lock protege o quê) e use-a no menor escopo possível.

Se você começar a cobrir erros com unchecked por conveniência da migração, terminará com código sem verificações, mas usando o selo do Swift 6.

Estrategicamente, não é preciso ativar tudo de uma vez. O modo de linguagem pode ser escolhido por módulo.

A abordagem padrão é de baixo para cima: migrar primeiro para o modo Swift 6 os módulos folha com poucas dependências, como utilitários e modelos, e deixar o target do app por último.

Os sinalizadores de upcoming feature do Xcode também ajudam a elevar apenas o nível de verificação antecipadamente e observar os avisos.

Entendendo a direção — por que passar por tudo isso?

Como a dor da migração é real, é importante entender exatamente o que justifica esse custo.

A promessa do Swift 6 é: se compila, não há data races. Categorias inteiras de crashes intermitentes irreproduzíveis e bugs de timing que só aparecem após o lançamento desaparecem em tempo de compilação.

É o mesmo caminho percorrido pela segurança de memória (optionals, ARC — Automatic Reference Counting, contagem automática de referências). A segurança de concorrência passa de “algo que se resolve escrevendo bem” para “algo garantido pela linguagem”.

Essa direção também estende a trajetória da série de filosofia: promover regras sujeitas a erros para o sistema de tipos, exigir anotações explícitas onde há custo (@unchecked, sending) e absorver o atrito da transição com adoção gradual (modos de linguagem por módulo).

Os procedimentos vistos no artigo sobre Swift Evolution continuam reduzindo o atrito ao adicionar mecanismos de amortecimento, como opções de isolamento padrão. Os avisos que você enfrenta agora são o ponto intermediário dessa transição.

Ilustração de placas das etapas de migração do Swift 5 até um Swift 6 normalizado
Transformar em struct→imutabilidade→actor→MainActor; usar unchecked por último, com justificativa

Resumo

  • Sendable marca tipos cujos valores podem ser usados simultaneamente com segurança ao atravessar limites de isolamento. Tipos por valor, actors e classes imutáveis são seguros; classes mutáveis concentram todo o perigo.
  • Closures @Sendable verificam suas capturas e impedem que se tornem rotas de contrabando para corridas.
  • Swift 6 strict concurrency promove essas verificações a erros. A explosão de erros não significa que o código piorou; significa que corridas potenciais vieram à tona.
  • Prioridade: transformar em struct → tornar imutável → promover a actor → declarar isolamento (@MainActor) → por fim, usar @unchecked Sendable com a justificativa documentada.
  • Migre de baixo para cima, começando pelos módulos folha, e eleve o modo de linguagem módulo a módulo.

A próxima parte encerra a série Concurrency com concorrência estruturada. Ela aborda a árvore de tarefas formada por Task, async let e TaskGroup, e como cancellation se propaga por essa árvore.


Referências

Continue lendo