Pra Caralho Acessível
Swift Concurrency

Finalmente entenda async/await, Tasks, e por que o compilador não para de reclamar com você.

Enorme agradecimento a Matt Massicotte por tornar a concorrência em Swift compreensível. Compilado por Pedro Piñera, co-fundador do Tuist. Encontrou um problema? Abra uma issue ou envie um PR.

Código Assíncrono: async/await

A maior parte do que os apps fazem é esperar. Buscar dados de um servidor - esperar a resposta. Ler um arquivo do disco - esperar pelos bytes. Consultar um banco de dados - esperar pelos resultados.

Antes do sistema de concorrência do Swift, você expressava essa espera com callbacks, delegates, ou Combine. Eles funcionam, mas callbacks aninhados ficam difíceis de acompanhar, e o Combine tem uma curva de aprendizado íngreme.

async/await dá ao Swift uma nova forma de lidar com espera. Em vez de callbacks, você escreve código que parece sequencial - ele pausa, espera, e continua. Por baixo dos panos, o runtime do Swift gerencia essas pausas de forma eficiente. Mas fazer seu app realmente continuar responsivo enquanto espera depende de onde o código roda, o que vamos cobrir mais tarde.

Uma função async é uma que pode precisar pausar. Você marca com async, e quando você a chama, você usa await para dizer "pause aqui até isso terminar":

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)  // Suspende aqui
    return try JSONDecoder().decode(User.self, from: data)
}

// Chamando
let user = try await fetchUser(id: 123)
// Código aqui roda depois que fetchUser completa

Seu código pausa em cada await - isso é chamado de suspensão. Quando o trabalho termina, seu código continua exatamente de onde parou. Suspensão dá ao Swift a oportunidade de fazer outro trabalho enquanto espera.

Esperando por eles

E se você precisa buscar várias coisas? Você poderia esperar uma por uma:

let avatar = try await fetchImage("avatar.jpg")
let banner = try await fetchImage("banner.jpg")
let bio = try await fetchBio()

Mas isso é lento - cada uma espera a anterior terminar. Use async let para rodá-las em paralelo:

func loadProfile() async throws -> Profile {
    async let avatar = fetchImage("avatar.jpg")
    async let banner = fetchImage("banner.jpg")
    async let bio = fetchBio()

    // As três estão buscando em paralelo!
    return Profile(
        avatar: try await avatar,
        banner: try await banner,
        bio: try await bio
    )
}

Cada async let começa imediatamente. O await coleta os resultados.

await precisa de async

Você só pode usar await dentro de uma função async.

Gerenciando Trabalho: Tasks

Uma Task é uma unidade de trabalho async que você pode gerenciar. Você escreveu funções async, mas uma Task é o que realmente as executa. É como você inicia código async a partir de código síncrono, e te dá controle sobre esse trabalho: esperar pelo resultado, cancelar, ou deixar rodar em segundo plano.

Digamos que você está construindo uma tela de perfil. Carregue o avatar quando a view aparecer usando o modificador .task, que cancela automaticamente quando a view desaparece:

struct ProfileView: View {
    @State private var avatar: Image?

    var body: some View {
        Group {
            if let avatar {
                avatar
            } else {
                ProgressView()
            }
        }
        .task { avatar = await downloadAvatar() }
    }
}

Se usuários podem alternar entre perfis, use .task(id:) para recarregar quando a seleção muda:

struct ProfileView: View {
    var userID: String
    @State private var avatar: Image?

    var body: some View {
        Group {
            if let avatar {
                avatar
            } else {
                ProgressView()
            }
        }
        .task(id: userID) { avatar = await downloadAvatar(for: userID) }
    }
}

Quando o usuário toca "Salvar", crie uma Task manualmente:

Button("Salvar") {
    Task { await saveProfile() }
}

Acessando resultados de Task

Quando você cria uma Task, recebe um handle. Use .value para esperar e obter o resultado:

let handle = Task {
    return await fetchUserData()
}
let userData = await handle.value  // Suspende até a task completar

Isso é útil quando você precisa do resultado mais tarde, ou quando quer guardar o handle da task e fazer await em outro lugar.

E se você precisa carregar o avatar, bio, e estatísticas tudo de uma vez? Use um TaskGroup para buscá-los em paralelo:

try await withThrowingTaskGroup(of: Void.self) { group in
    group.addTask { avatar = try await downloadAvatar(for: userID) }
    group.addTask { bio = try await fetchBio(for: userID) }
    group.addTask { stats = try await fetchStats(for: userID) }
    try await group.waitForAll()
}

Tasks dentro de um grupo são tasks filhas, ligadas ao pai. Algumas coisas para saber:

  • Cancelamento se propaga: cancele o pai, e todas as filhas são canceladas também
  • Erros: um erro lançado cancela irmãs e relança, mas só quando você consome resultados com next(), waitForAll(), ou iteração
  • Ordem de conclusão: resultados chegam conforme tasks terminam, não na ordem que você as adicionou
  • Espera por todas: o grupo não retorna até que cada filha complete ou seja cancelada

Isso é concorrência estruturada: trabalho organizado em uma árvore que é fácil de entender e limpar.

Onde as Coisas Rodam: De Threads a Domínios de Isolamento

Até agora falamos sobre quando código roda (async/await) e como organizá-lo (Tasks). Agora: onde ele roda, e como mantemos seguro?

A maioria dos apps só espera

A maior parte do código de apps é I/O-bound. Você busca dados de uma rede, await uma resposta, decodifica, e exibe. Se você tem múltiplas operações de I/O para coordenar, você recorre a tasks e task groups. O trabalho real de CPU é mínimo. A thread principal consegue lidar bem com isso porque await suspende sem bloquear.

Mas cedo ou tarde, você vai ter trabalho CPU-bound: parsear um arquivo JSON gigante, processar imagens, rodar cálculos complexos. Esse trabalho não espera por nada externo. Só precisa de ciclos de CPU. Se você rodar na thread principal, sua UI congela. É aí que "onde o código roda" realmente importa.

O Mundo Antigo: Muitas Opções, Nenhuma Segurança

Antes do sistema de concorrência do Swift, você tinha várias formas de gerenciar execução:

Abordagem O que faz Trade-offs
Thread Controle direto de thread Baixo nível, propenso a erros, raramente necessário
GCD Dispatch queues com closures Simples mas sem cancelamento, fácil causar explosão de threads
OperationQueue Dependências de tasks, cancelamento, KVO Mais controle mas verboso e pesado
Combine Streams reativos Ótimo para streams de eventos, curva de aprendizado íngreme

Todos funcionavam, mas segurança era totalmente sua responsabilidade. O compilador não podia ajudar se você esquecesse de despachar para main, ou se duas queues acessassem os mesmos dados simultaneamente.

O Problema: Data Races

Um data race acontece quando duas threads acessam a mesma memória ao mesmo tempo, e pelo menos uma está escrevendo:

var count = 0

DispatchQueue.global().async { count += 1 }
DispatchQueue.global().async { count += 1 }

// Comportamento indefinido: crash, corrupção de memória, ou valor errado

Data races são comportamento indefinido. Eles podem crashar, corromper memória, ou silenciosamente produzir resultados errados. Seu app funciona bem em testes, depois crasha aleatoriamente em produção. Ferramentas tradicionais como locks e semáforos ajudam, mas são manuais e propensas a erros.

Concorrência amplifica o problema

Quanto mais concorrente seu app é, mais prováveis data races se tornam. Um app iOS simples pode se safar com thread safety desleixado. Um servidor web lidando com milhares de requisições simultâneas vai crashar constantemente. É por isso que a segurança em tempo de compilação do Swift importa mais em ambientes de alta concorrência.

A Mudança: De Threads para Isolamento

O modelo de concorrência do Swift faz uma pergunta diferente. Em vez de "em qual thread isso deveria rodar?", ele pergunta: "quem tem permissão para acessar esses dados?"

Isso é isolamento. Em vez de despachar trabalho manualmente para threads, você declara fronteiras ao redor dos dados. O compilador impõe essas fronteiras em tempo de build, não em runtime.

Por baixo dos panos

Swift Concurrency é construído em cima do libdispatch (o mesmo runtime que GCD). A diferença é a camada em tempo de compilação: actors e isolamento são impostos pelo compilador, enquanto o runtime lida com agendamento em um thread pool cooperativo limitado à contagem de cores da sua CPU.

Os Três Domínios de Isolamento

1. MainActor

@MainActor é um global actor que representa o domínio de isolamento da thread principal. É especial porque frameworks de UI (UIKit, AppKit, SwiftUI) requerem acesso à thread principal.

@MainActor
class ViewModel {
    var items: [Item] = []  // Protegido pelo isolamento do MainActor
}

Quando você marca algo com @MainActor, você não está dizendo "despache isso para a thread principal." Você está dizendo "isso pertence ao domínio de isolamento do main actor." O compilador impõe que qualquer coisa acessando deve estar no MainActor ou await para cruzar a fronteira.

Na dúvida, use @MainActor

Para a maioria dos apps, marcar seus ViewModels com @MainActor é a escolha certa. Preocupações com performance geralmente são exageradas. Comece aqui, otimize só se você medir problemas reais.

2. Actors

Um actor protege seu próprio estado mutável. Ele garante que apenas um pedaço de código pode acessar seus dados por vez:

actor BankAccount {
    var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount  // Seguro: actor garante acesso exclusivo
    }
}

// De fora, você deve await para cruzar a fronteira
await account.deposit(100)

Actors não são threads. Um actor é uma fronteira de isolamento. O runtime do Swift decide qual thread realmente executa código do actor. Você não controla isso, e não precisa.

3. Nonisolated

Código marcado como nonisolated opta por sair do isolamento do actor. Pode ser chamado de qualquer lugar sem await, mas não pode acessar o estado protegido do actor:

actor BankAccount {
    var balance: Double = 0

    nonisolated func bankName() -> String {
        "Acme Bank"  // Nenhum estado do actor acessado, seguro chamar de qualquer lugar
    }
}

let name = account.bankName()  // Não precisa de await

Approachable Concurrency: Menos Fricção

Approachable Concurrency simplifica o modelo mental com duas configurações do Xcode:

  • SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor: Tudo roda no MainActor a menos que você diga o contrário
  • SWIFT_APPROACHABLE_CONCURRENCY = YES: Funções async nonisolated ficam no actor do chamador em vez de pular para uma thread de segundo plano

Novos projetos Xcode 26 têm ambos habilitados por padrão. Quando você precisa de trabalho intensivo de CPU fora da thread principal, use @concurrent.

// Roda no MainActor (o padrão)
func updateUI() async { }

// Roda em thread de segundo plano (opt-in)
@concurrent func processLargeFile() async { }

O Prédio de Escritórios

Pense no seu app como um prédio de escritórios. Cada domínio de isolamento é um escritório privado com uma fechadura na porta. Só uma pessoa pode estar dentro por vez, trabalhando com os documentos naquele escritório.

  • MainActor é a recepção - onde todas as interações com clientes acontecem. Só existe uma, e ela lida com tudo que o usuário vê.
  • Tipos actor são escritórios de departamento - Contabilidade, Jurídico, RH. Cada um protege seus próprios documentos sensíveis.
  • Código nonisolated é o corredor - espaço compartilhado por onde qualquer um pode andar, mas nenhum documento privado fica lá.

Você não pode simplesmente invadir o escritório de alguém. Você bate (await) e espera eles te deixarem entrar.

O Que Pode Cruzar Domínios de Isolamento: Sendable

Domínios de isolamento protegem dados, mas eventualmente você precisa passar dados entre eles. Quando você faz isso, Swift verifica se é seguro.

Pense nisso: se você passa uma referência para uma classe mutável de um actor para outro, ambos actors poderiam modificá-la simultaneamente. Isso é exatamente o data race que estamos tentando prevenir. Então Swift precisa saber: esses dados podem ser compartilhados com segurança?

A resposta é o protocolo Sendable. É um marcador que diz ao compilador "esse tipo é seguro para passar através de fronteiras de isolamento":

  • Tipos Sendable podem cruzar com segurança (tipos de valor, dados imutáveis, actors)
  • Tipos Non-Sendable não podem (classes com estado mutável)
// Sendable - é um tipo de valor, cada lugar recebe uma cópia
struct User: Sendable {
    let id: Int
    let name: String
}

// Non-Sendable - é uma classe com estado mutável
class Counter {
    var count = 0  // Dois lugares modificando isso = desastre
}

Tornando Tipos Sendable

Swift automaticamente infere Sendable para muitos tipos:

  • Structs e enums com apenas propriedades Sendable são implicitamente Sendable
  • Actors são sempre Sendable porque protegem seu próprio estado
  • Tipos @MainActor são Sendable porque MainActor serializa acesso

Para classes, é mais difícil. Uma classe pode conformar com Sendable apenas se for final e todas suas propriedades armazenadas forem imutáveis:

final class APIConfig: Sendable {
    let baseURL: URL      // Imutável
    let timeout: Double   // Imutável
}

Se você tem uma classe que é thread-safe por outros meios (locks, atomics), você pode usar @unchecked Sendable para dizer ao compilador "confia em mim":

final class ThreadSafeCache: @unchecked Sendable {
    private let lock = NSLock()
    private var storage: [String: Data] = [:]
}

@unchecked Sendable é uma promessa

O compilador não vai verificar thread safety. Se você estiver errado, você terá data races. Use com moderação.

Approachable Concurrency: Menos Fricção

Com Approachable Concurrency, erros de Sendable se tornam muito mais raros:

  • Se código não cruza fronteiras de isolamento, você não precisa de Sendable
  • Funções async ficam no actor do chamador em vez de pular para uma thread de segundo plano
  • O compilador é mais esperto em detectar quando valores são usados com segurança

Habilite configurando SWIFT_DEFAULT_ACTOR_ISOLATION como MainActor e SWIFT_APPROACHABLE_CONCURRENCY como YES. Novos projetos Xcode 26 têm ambos habilitados por padrão. Quando você precisa de paralelismo, marque funções como @concurrent e então pense em Sendable.

Fotocópias vs. Documentos Originais

Voltando ao prédio de escritórios. Quando você precisa compartilhar informações entre departamentos:

  • Fotocópias são seguras - Se o Jurídico faz uma cópia de um documento e envia para a Contabilidade, ambos têm sua própria cópia. Podem rabiscar nelas, modificar, tanto faz. Sem conflito.
  • Contratos originais assinados devem ficar parados - Se dois departamentos pudessem modificar o original, caos se instala. Quem tem a versão real?

Tipos Sendable são como fotocópias: seguros para compartilhar porque cada lugar recebe sua própria cópia independente (tipos de valor) ou porque são imutáveis (ninguém pode modificá-los). Tipos non-Sendable são como contratos originais: passá-los por aí cria o potencial para modificações conflitantes.

Como o Isolamento é Herdado

Você viu que domínios de isolamento protegem dados, e Sendable controla o que cruza entre eles. Mas como código acaba em um domínio de isolamento em primeiro lugar?

Quando você chama uma função ou cria um closure, isolamento flui através do seu código. Com Approachable Concurrency, seu app começa no MainActor, e esse isolamento se propaga para o código que você chama, a menos que algo explicitamente mude isso. Entender esse fluxo te ajuda a prever onde código roda e por que o compilador às vezes reclama.

Chamadas de Função

Quando você chama uma função, seu isolamento determina onde ela roda:

@MainActor func updateUI() { }      // Sempre roda no MainActor
func helper() { }                    // Herda isolamento do chamador
@concurrent func crunch() async { }  // Explicitamente roda fora do actor

Com Approachable Concurrency, a maior parte do seu código herda isolamento do MainActor. A função roda onde o chamador roda, a menos que ela explicitamente opte por sair.

Closures

Closures herdam isolamento do contexto onde são definidos:

@MainActor
class ViewModel {
    func setup() {
        let closure = {
            // Herda MainActor do ViewModel
            self.updateUI()  // Seguro, mesmo isolamento
        }
        closure()
    }
}

É por isso que closures de ação de Button do SwiftUI podem atualizar @State com segurança: eles herdam isolamento do MainActor da view.

Tasks

Um Task { } herda isolamento do actor de onde é criado:

@MainActor
class ViewModel {
    func doWork() {
        Task {
            // Herda isolamento do MainActor
            self.updateUI()  // Seguro, não precisa de await
        }
    }
}

Isso geralmente é o que você quer. A task roda no mesmo actor que o código que a criou.

Quebrando Herança: Task.detached

Às vezes você quer uma task que não herda nenhum contexto:

@MainActor
class ViewModel {
    func doHeavyWork() {
        Task.detached {
            // Sem isolamento de actor, roda no pool cooperativo
            let result = await self.expensiveCalculation()
            await MainActor.run {
                self.data = result  // Explicitamente volta
            }
        }
    }
}

Task e Task.detached são antipadrões

As tasks que você agenda com Task { ... } não são gerenciadas. Não há como cancelá-las ou saber quando terminam, se é que terminam. Não há como acessar seu valor de retorno ou saber se encontraram um erro. Na maioria dos casos, é melhor usar tasks gerenciadas por .task ou TaskGroup, como explicado na seção "Erros Comuns".

Task.detached deve ser seu último recurso. Tasks detached não herdam prioridade, valores task-local, ou contexto de actor. Se você precisa de trabalho intensivo de CPU fora do main actor, marque a função como @concurrent em vez disso.

Preservando Isolamento em Utilitários Async

Às vezes você escreve uma função async genérica que aceita um closure - um wrapper, um helper de retry, um escopo de transação. O chamador passa um closure, sua função executa. Simples, certo?

func measure<T>(
    _ label: String,
    block: () async throws -> T
) async rethrows -> T {
    let start = ContinuousClock.now
    let result = try await block()
    print("\(label): \(ContinuousClock.now - start)")
    return result
}

Mas quando você chama isso de um contexto @MainActor, Swift reclama:

Sending value of non-Sendable type '() async throws -> T' risks causing data races

O que está acontecendo? Seu closure captura estado do MainActor, mas measure é nonisolated. Swift vê um closure não Sendable cruzando uma fronteira de isolamento - exatamente o que foi projetado para prevenir.

A solução mais simples é nonisolated(nonsending). Isso diz ao Swift que a função deve ficar no executor que a chamou:

nonisolated(nonsending)
func measure<T>(
    _ label: String,
    block: () async throws -> T
) async rethrows -> T {
    let start = ContinuousClock.now
    let result = try await block()
    print("\(label): \(ContinuousClock.now - start)")
    return result
}

Agora a função inteira roda no executor do chamador. Chame do MainActor, fica no MainActor. Chame de um actor customizado, fica lá. O closure nunca cruza uma fronteira de isolamento, então não precisa de verificação Sendable.

Quando usar cada abordagem

nonisolated(nonsending) - A escolha simples. Só adicione o atributo. Use quando você só precisa ficar no executor do chamador.

isolation: isolated (any Actor)? = #isolation - A escolha explícita. Adiciona um parâmetro que dá acesso à instância do actor. Use quando precisar passar o contexto de isolamento para outras funções ou inspecionar em qual actor você está.

Se você precisa de acesso explícito ao actor, use um parâmetro #isolation:

func measure<T>(
    isolation: isolated (any Actor)? = #isolation,
    _ label: String,
    block: () async throws -> T
) async rethrows -> T {
    let start = ContinuousClock.now
    let result = try await block()
    print("\(label): \(ContinuousClock.now - start)")
    return result
}

Ambas as abordagens são essenciais para construir utilitários async que parecem naturais de usar. Sem elas, chamadores precisariam tornar seus closures @Sendable ou pular obstáculos para satisfazer o compilador.

Andando Pelo Prédio

Quando você está no escritório da recepção (MainActor), e você chama alguém para te ajudar, essa pessoa vem para o seu escritório. Ela herda sua localização. Se você cria uma task ("vai fazer isso pra mim"), esse assistente começa no seu escritório também.

A única forma de alguém acabar em um escritório diferente é se eles explicitamente forem para lá: "Preciso trabalhar na Contabilidade pra isso" (actor), ou "Vou lidar com isso no escritório dos fundos" (@concurrent).

Juntando Tudo

Vamos dar um passo atrás e ver como todas as peças se encaixam.

Swift Concurrency pode parecer um monte de conceitos: async/await, Task, actors, MainActor, Sendable, domínios de isolamento. Mas existe realmente só uma ideia no centro de tudo: isolamento é herdado por padrão.

Com Approachable Concurrency habilitado, seu app começa no MainActor. Esse é seu ponto de partida. A partir daí:

  • Toda função que você chama herda esse isolamento
  • Todo closure que você cria captura esse isolamento
  • Toda Task { } que você cria herda esse isolamento

Você não precisa anotar nada. Você não precisa pensar em threads. Seu código roda no MainActor, e o isolamento simplesmente se propaga pelo seu programa automaticamente.

Quando você precisa sair dessa herança, você faz explicitamente:

  • @concurrent diz "rode isso em uma thread de segundo plano"
  • actor diz "esse tipo tem seu próprio domínio de isolamento"
  • Task.detached { } diz "comece do zero, não herde nada"

E quando você passa dados entre domínios de isolamento, Swift verifica se é seguro. É pra isso que Sendable serve: marcar tipos que podem cruzar fronteiras com segurança.

É isso. Esse é o modelo todo:

  1. Isolamento se propaga do MainActor através do seu código
  2. Você opta por sair explicitamente quando precisa de trabalho em segundo plano ou estado separado
  3. Sendable guarda as fronteiras quando dados cruzam entre domínios

Quando o compilador reclama, ele está te dizendo que uma dessas regras foi violada. Trace a herança: de onde veio o isolamento? Onde o código está tentando rodar? Que dados estão cruzando uma fronteira? A resposta geralmente é óbvia quando você faz a pergunta certa.

Para Onde Ir Daqui

A boa notícia: você não precisa dominar tudo de uma vez.

A maioria dos apps só precisa do básico. Marque seus ViewModels com @MainActor, use async/await para chamadas de rede, e crie Task { } quando precisar iniciar trabalho async de um toque de botão. É isso. Isso cobre 80% dos apps do mundo real. O compilador vai te dizer se você precisa de mais.

Quando você precisa de trabalho paralelo, recorra a async let para buscar múltiplas coisas de uma vez, ou TaskGroup quando o número de tasks é dinâmico. Aprenda a lidar com cancelamento graciosamente. Isso cobre apps com carregamento de dados complexo ou features em tempo real.

Padrões avançados vêm depois, se algum dia. Actors customizados para estado mutável compartilhado, @concurrent para processamento intensivo de CPU, entendimento profundo de Sendable. Isso é código de framework, Swift server-side, apps desktop complexos. A maioria dos desenvolvedores nunca precisa desse nível.

Comece simples

Não otimize para problemas que você não tem. Comece com o básico, lance seu app, e adicione complexidade só quando encontrar problemas reais. O compilador vai te guiar.

Cuidado: Erros Comuns

Pensar que async = segundo plano

// Isso AINDA bloqueia a thread principal!
@MainActor
func slowFunction() async {
    let result = expensiveCalculation()  // Trabalho síncrono = bloqueante
    data = result
}

async significa "pode pausar." O trabalho real ainda roda onde quer que rode. Use @concurrent (Swift 6.2) ou Task.detached para trabalho pesado de CPU.

Criar actors demais

// Over-engineered
actor NetworkManager { }
actor CacheManager { }
actor DataManager { }

// Melhor - a maioria das coisas pode viver no MainActor
@MainActor
class AppState { }

Você só precisa de um actor customizado quando tem estado mutável compartilhado que não pode viver no MainActor. Regra do Matt Massicotte: introduza um actor apenas quando (1) você tem estado non-Sendable, (2) operações nesse estado devem ser atômicas, e (3) essas operações não podem rodar em um actor existente. Se você não consegue justificar, use @MainActor em vez disso.

Fazer tudo Sendable

Nem tudo precisa cruzar fronteiras. Se você está adicionando @unchecked Sendable em todo lugar, dê um passo atrás e pergunte se os dados realmente precisam se mover entre domínios de isolamento.

Usar MainActor.run quando você não precisa

// Desnecessário
Task {
    let data = await fetchData()
    await MainActor.run {
        self.data = data
    }
}

// Melhor - apenas faça a função @MainActor
@MainActor
func loadData() async {
    self.data = await fetchData()
}

MainActor.run raramente é a solução certa. Se você precisa de isolamento MainActor, anote a função com @MainActor em vez disso. É mais claro e o compilador pode te ajudar mais. Veja a opinião do Matt sobre isso.

Bloquear o thread pool cooperativo

// NUNCA faça isso - risco de deadlock
func badIdea() async {
    let semaphore = DispatchSemaphore(value: 0)
    Task {
        await doWork()
        semaphore.signal()
    }
    semaphore.wait()  // Bloqueia uma thread cooperativa!
}

O thread pool cooperativo do Swift tem threads limitadas. Bloquear uma com DispatchSemaphore, DispatchGroup.wait(), ou chamadas similares pode causar deadlocks. Se você precisa fazer ponte entre código sync e async, use async let ou reestruture para ficar totalmente async.

Criar tasks não gerenciadas

Tasks que você cria manualmente com Task { ... } ou Task.detached { ... } não são gerenciadas. Depois de criar tasks não gerenciadas, você não pode controlá-las. Você não pode cancelá-las se a task da qual você as iniciou for cancelada. Você não pode saber se elas terminaram seu trabalho, se lançaram um erro, ou coletar seu valor de retorno. Iniciar tal task é como jogar uma garrafa no mar esperando que ela entregue sua mensagem ao destino, sem nunca ver essa garrafa de novo.

O Prédio de Escritórios

Uma Task é como atribuir trabalho a um funcionário. O funcionário lida com a solicitação (incluindo esperar por outros escritórios) enquanto você continua com seu trabalho imediato.

Depois de despachar trabalho para o funcionário, você não tem meios de se comunicar com ela. Você não pode dizer para ela parar o trabalho ou saber se ela terminou e qual foi o resultado desse trabalho.

O que você realmente quer é dar ao funcionário um walkie-talkie para se comunicar com ela enquanto ela lida com a solicitação. Com o walkie-talkie, você pode dizer para ela parar, ou ela pode te dizer quando encontrar um erro, ou pode reportar o resultado da solicitação que você deu a ela.

Em vez de criar tasks não gerenciadas, use concorrência Swift para manter o controle das subtasks que você cria. Use TaskGroup para gerenciar (um grupo de) subtask(s). Swift fornece algumas funções withTaskGroup() { group in ... } para ajudar a criar grupos de tasks.

func doWork() async {

    // isso retornará quando todas as subtasks retornarem, lançarem um erro, ou forem canceladas
    let result = try await withThrowingTaskGroup() { group in
        group.addTask {
            try await self.performAsyncOperation1()
        }
        group.addTask {
            try await self.performAsyncOperation2()
        }
        // espere e colete os resultados das tasks aqui
    }
}

func performAsyncOperation1() async throws -> Int {
    return 1
}
func performAsyncOperation2() async throws -> Int {
    return 2
}

Para coletar os resultados das tasks filhas do grupo, você pode usar um loop for-await-in:

var sum = 0
for await result in group {
    sum += result
}
// sum == 3

Você pode aprender mais sobre TaskGroup na documentação do Swift.

Nota sobre Tasks e SwiftUI.

Ao escrever uma UI, você frequentemente quer iniciar tasks assíncronas de um contexto síncrono. Por exemplo, você quer carregar uma imagem assincronamente como resposta a um toque em um elemento de UI. Iniciar tasks assíncronas de um contexto síncrono não é possível em Swift. Por isso você vê soluções envolvendo Task { ... }, que introduz tasks não gerenciadas.

Você não pode usar TaskGroup de um modifier síncrono do SwiftUI porque withTaskGroup() é uma função async também e suas funções relacionadas também são.

Como alternativa, SwiftUI oferece um modifier assíncrono que você pode usar para iniciar operações assíncronas. O modifier .task { }, que já mencionamos, aceita uma função () async -> Void, ideal para chamar outras funções async. Está disponível em toda View. É acionado antes da view aparecer e as tasks que ele cria são gerenciadas e vinculadas ao ciclo de vida da view, significando que as tasks são canceladas quando a view desaparece.

Voltando ao exemplo de tocar-para-carregar-uma-imagem: em vez de criar uma task não gerenciada para chamar uma função assíncrona loadImage() de uma função síncrona .onTap() { ... }, você pode alternar uma flag no gesto de tap e usar o modifier task(id:) para carregar imagens assincronamente quando o valor do id (a flag) mudar.

Aqui está um exemplo:

struct ContentView: View {

    @State private var shouldLoadImage = false

    var body: some View {
        Button("Clique Aqui!") {
            // alterna a flag
            shouldLoadImage = !shouldLoadImage
        }
        // a View gerencia a subtask
        // ela inicia antes da view ser exibida
        // e para quando a view é ocultada
        .task(id: shouldLoadImage) {
            // quando o valor da flag muda, SwiftUI reinicia a task
            guard shouldLoadImage else { return }
            await loadImage()
        }
    }
}

Cola Rápida: Referência

Palavra-chave O que faz
async Função pode pausar
await Pause aqui até terminar
Task { } Inicia trabalho async, herda contexto
Task.detached { } Inicia trabalho async, sem contexto herdado
@MainActor Roda na thread principal
actor Tipo com estado mutável isolado
nonisolated Opta por sair do isolamento do actor
nonisolated(nonsending) Fica no executor do chamador
Sendable Seguro para passar entre domínios de isolamento
@concurrent Sempre roda em segundo plano (Swift 6.2+)
#isolation Captura o isolamento do chamador como parâmetro
async let Inicia trabalho paralelo
TaskGroup Trabalho paralelo dinâmico

Leitura Adicional

Blog do Matt Massicotte (Altamente Recomendado)

Ferramentas

  • Tuist - Desenvolva mais rápido com equipes e projetos maiores

Skill para Agentes IA

Quer que seu assistente de código IA entenda Swift Concurrency? Fornecemos um arquivo SKILL.md que empacota esses modelos mentais para agentes IA como Claude Code, Codex, Amp, OpenCode e outros.

Outras skills

O que é um Skill?

Um skill é um arquivo markdown que ensina conhecimentos especializados para agentes de código IA. Quando você adiciona o skill de Swift Concurrency ao seu agente, ele automaticamente aplica esses conceitos quando te ajuda a escrever código Swift assíncrono.

Como Usar

Escolha seu agente e execute os comandos:

# Skill pessoal (todos os seus projetos)
mkdir -p ~/.claude/skills/swift-concurrency
curl -o ~/.claude/skills/swift-concurrency/SKILL.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Skill de projeto (apenas este projeto)
mkdir -p .claude/skills/swift-concurrency
curl -o .claude/skills/swift-concurrency/SKILL.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Instruções globais (todos os seus projetos)
curl -o ~/.codex/AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Instruções de projeto (apenas este projeto)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Instruções de projeto (recomendado)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Regras globais (todos os seus projetos)
mkdir -p ~/.config/opencode
curl -o ~/.config/opencode/AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Regras de projeto (apenas este projeto)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md

O skill inclui a analogia do Edifício de Escritórios, padrões de isolamento, guia de Sendable, erros comuns e tabelas de referência rápida. Seu agente usará esse conhecimento automaticamente quando você trabalhar com código Swift Concurrency.