Pular para o conteúdo
Development

A armadilha do isolamento de actor em "só tira isso da main thread"

Por Victor Da Luz
iosswiftswift-concurrencydev-logdeep-cut-atlas

Esse app foi renomeado depois para Deep Cut Atlas. Abaixo ele é chamado de “Discoverer” o tempo todo, porque foi assim que se chamava no dia em que isso aconteceu.

Passei parte desta semana corrigindo trabalho desperdiçado na aba Discover, que mostra faixas menos conhecidas de artistas da sua biblioteca. Uma correção parecia trivial no papel e virou um pequeno tour pelas regras de concorrência mais rígidas do Swift 6.

O cenário: o cache em disco do Discover fazia uma decodificação de JSON bloqueante direto na main thread, toda vez que a aba checava se o cache tinha ficado desatualizado. Não é enorme sozinho, mas acontecia a cada vez que o app voltava ao primeiro plano, e o arquivo cresce junto com sua biblioteca. A correção parecia óbvia: envolver a decodificação numa detached task para rodar fora da main actor.

Não compilou.

main actor-isolated conformance of 'DiscoveryFeed' to 'Decodable' cannot be used in nonisolated context

Esse app é construído com isolamento de actor padrão configurado para MainActor, o que significa que todo tipo simples no módulo fica implicitamente amarrado à main actor, a menos que você diga o contrário. Essa parte eu já sabia. O que eu não tinha internalizado é que isso também vale para código sintetizado pelo compilador. A conformidade a Codable gerada automaticamente de uma struct simples, os métodos encode e decode que você nunca escreve à mão, herda esse mesmo isolamento. As propriedades armazenadas da minha struct eram todas Sendable. Não importou. A struct em si, e portanto seu decoder sintetizado, estava isolada na main actor, e uma detached task não está na main actor por definição.

A correção é uma única palavra: marcar o tipo como nonisolated. Não um método, o tipo inteiro. E isso é contagioso na direção que você imaginaria: minha struct de cache de nível superior guardava um array de uma struct aninhada, que por sua vez guardava um array de um modelo de álbum usado em outras partes do app. As três precisaram da anotação, porque o Swift verifica o isolamento de qualquer tipo que esteja de fato fazendo a codificação, não só de suas propriedades finais.

Depois veio uma segunda barreira, sem relação com a primeira. Eu queria que duas chamadas de rede independentes começassem ao mesmo tempo em vez de uma depois da outra, então recorri ao async let, o jeito de livro-texto de fazer exatamente isso. Erro de compilação de novo, dessa vez sobre um “non-Sendable type… cannot exit main actor-isolated context.” Acontece que async let sempre cria uma child task de verdade por baixo dos panos, e uma child task tem as mesmas exigências de qualquer outra task concorrente: tudo que ela captura precisa ser comprovadamente seguro para repassar. Meu serviço era isolado na main actor por design, o que normalmente o torna seguro de chamar de qualquer lugar do app, mas essa segurança não é transferida automaticamente para uma task totalmente nova, mesmo uma que vai chamar de volta a mesma actor imediatamente. A correção que funcionou foi mais feia do que eu queria: uma Task { } simples e não estruturada, que herda o contexto da actor que a chamou em vez de pedir um novo. Menos elegante que async let, mas é o padrão que o resto desse código já usa pelo mesmo motivo, então pelo menos agora está consistente.

Nenhum dos dois erros era realmente sobre o código estar errado. Os dois eram sobre código que “parece puro” mas depende secretamente de uma configuração de build que quase ninguém olha. A lição que eu continuo reaprendendo com concorrência no Swift 6: não raciocine sobre isolamento a partir da aparência de um tipo na tela. Raciocine a partir do padrão de fato usado no projeto, e verifique toda fronteira que um valor atravessa, porque o compilador vai deixar você escrever algo que o sistema de tipos já não gostava, até o momento em que você tentar rodar isso em outro lugar.

Leitura relacionada

Você também pode achar útil

AdGuard

AdGuard para iOS

Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.

Como afiliado da AdGuard, ganho com compras qualificadas.

Saiba mais
NordPass

NordPass

Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.

Como afiliado da NordPass, ganho com compras qualificadas.

Saiba mais
RackNerd

RackNerd VPS

Hospedagem VPS econômica para serviços leves que funcionam continuamente.

Como afiliado da RackNerd, ganho com compras qualificadas.

Saiba mais