Nos artigos sobre tratamento de erros e closures, mencionei várias vezes a ressalva “depois de async/await”. Este é o artigo principal.
Este é o primeiro artigo da série Swift Concurrency que abre a série avançada. Começamos pelo que async/await realmente resolve e por qual princípio funciona.
A sintaxe em si pode ser aprendida em um dia: adicionar async à função e await à chamada.
A dificuldade vem depois. Perguntas como “a thread para no await?” e “em qual thread uma função async é executada?”
Sem respostas claras para essas perguntas, o código de concorrência continua sendo uma questão de decorar fórmulas.
O objetivo deste artigo é responder precisamente às duas perguntas.
O verdadeiro problema dos callbacks — não a indentação, mas a cegueira do compilador
Antes de async/await, o código assíncrono usava completion handlers, como vimos no artigo sobre closures.
A indentação do “inferno de callbacks” costuma ser apontada como problema, mas ela é apenas o sintoma. A raiz é que o compilador não consegue enxergar o fluxo.
func loadProfile(completion: @escaping (Result<Profile, Error>) -> Void) {
fetchUser { result in
switch result {
case .success(let user):
fetchAvatar(user.avatarURL) { avatarResult in
// completion E se você esquecer de chamar?
}
case .failure(let error):
completion(.failure(error))
}
}
}
Mesmo que você deixe de chamar completion em algum caminho, chame duas vezes ou use a thread errada, o compilador não diz nada.
Esse é o resultado de substituir o conceito básico de retorno da linguagem pela convenção de chamar uma closure. As garantias antes fornecidas pela linguagem—todo caminho retorna um valor e os erros são propagados—passaram a depender da disciplina do desenvolvedor.
async/await traz o código assíncrono de volta à jurisdição da linguagem.
func loadProfile() async throws -> Profile {
let user = try await fetchUser()
let avatar = try await fetchAvatar(user.avatarURL)
return Profile(user: user, avatar: avatar)
}
Retorno é return e erro é throws. O compilador volta a verificar se todo caminho produz um valor ou lança um erro.
As regras de do-catch e propagação explicadas no artigo sobre erros continuam funcionando no código assíncrono. Não é apenas açúcar sintático; é recuperar a visão perdida do compilador.
O significado exato de suspension — quem para é a função, não a thread
Primeira pergunta: a thread para no await?
Não. A função pausa, e a thread é liberada para fazer outro trabalho.
Essa distinção é a frase mais importante de Swift Concurrency.
No ponto de await, a função armazena seu estado de progresso—variáveis locais e o ponto de execução—no heap, não na pilha, e devolve a thread que ocupava.
Isso é chamado de suspension. Quando o trabalho aguardado termina, a execução é retomada com o estado armazenado.
Não há garantia de que a retomada use a mesma thread. As linhas antes e depois de await na mesma função podem executar em threads diferentes.
Graças a esse design, o runtime de concorrência do Swift usa um pool cooperativo de threads. Ele cria aproximadamente tantas threads quanto núcleos de CPU, e as funções cedem essas threads umas às outras a cada await.
Esse é o contraste com a época do GCD (Grand Central Dispatch), em que centenas de threads eram criadas e colocadas para dormir (blocking). Como as threads não dormem, explosões de threads e trocas de contexto desperdiçadas são reduzidas estruturalmente.
Daqui surge uma regra prática: não bloqueie nas threads do pool cooperativo.
Esperar em um semáforo ou chamar sleep faz uma thread inteira do pool, projetado para ceder, dormir. Com 4 núcleos e cerca de 4 threads, fazer uma dormir equivale a parar um quarto do sistema.
“await cede; block é proibido” é a primeira regra do pool cooperativo.
Em qual thread ele roda — pergunte sobre isolamento, não sobre threads
Segunda pergunta: em qual thread uma função async é executada?
A resposta de Swift Concurrency é “abandone essa pergunta”. Threads são recursos gerenciados pelo runtime; o desenvolvedor especifica o isolamento, ou seja, “a qual contexto de execução serial este código pertence?”
O isolamento mais conhecido é MainActor. Funções e tipos marcados com @MainActor têm execução garantida na thread principal.
Isso é diferente de espalhar DispatchQueue.main.async pelo código de atualização da UI por intuição. O requisito “somente na principal” é declarado no sistema de tipos e verificado pelo compilador.
UIViewController e SwiftUI View já são declarados com @MainActor. O código de uma view recebe isolamento principal automaticamente.
Por outro lado, uma função async sem indicação de isolamento não pertence a nenhum actor específico (nonisolated) e é executada em algum ponto do pool cooperativo.
Para tirar um cálculo pesado da thread principal, você não precisa escrever código que o “envie para uma thread em segundo plano”. Em Swift, declare que o trabalho não pertence ao isolamento de MainActor.
Quando necessário, abra um novo contexto assíncrono com Task. (O uso estruturado de Task é abordado no artigo 4 desta série.)
A linguagem também fornece uma ponte para APIs de callback existentes. withCheckedThrowingContinuation permite encapsular uma API de callback como uma função async.
Chamar resume de continuation exatamente uma vez é o contrato, e a versão “Checked” detecta violações em runtime. É a primeira ferramenta de bridging que você deve dominar ao lidar com SDKs legados.
A armadilha do await sequencial — concorrência não é de graça
O código loadProfile acima contém uma armadilha de desempenho. E se a outra requisição não tiver relação com fetchUser?
// Sequencial: o avatar só começa depois que o banner termina (total de 2s)
let avatar = try await fetchAvatar() // 1s
let banner = try await fetchBanner() // 1s
await significa “esperar aqui”, então, escrito assim, as duas requisições ficam enfileiradas sequencialmente. Se as tarefas não dependem umas das outras, o correto é iniciá-las simultaneamente com async let.
// Concorrente: as duas requisições rodam juntas (total de 1s)
async let avatar = fetchAvatar()
async let banner = fetchBanner()
let profile = try await Profile(avatar: avatar, banner: banner)
async/await não paraleliza automaticamente. Escolher entre execução sequencial e concorrente continua sendo responsabilidade do designer.
A evolução é que essa escolha virou uma diferença de uma linha de sintaxe, em vez de composição de callbacks. O uso sistemático de async let e TaskGroup é tratado em detalhes no artigo sobre concorrência estruturada.
Resumo
- O verdadeiro problema dos callbacks não era a indentação, mas a cegueira do compilador. async/await devolve retornos e propagação de erros à jurisdição da linguagem, restaurando as verificações do compilador.
- No await, quem para é a função, não a thread. O estado da função é armazenado no heap, a thread é devolvida e não há garantia de retomada na mesma thread.
- O runtime é um pool cooperativo de threads. Por isso, bloquear dentro dele—com semáforos ou sleep—é proibido.
- A pergunta não é “em qual thread?”, mas “em qual isolamento?”. A UI é garantida por declarações @MainActor, e APIs de callback são encapsuladas com continuations.
- await não paraleliza automaticamente. Inicie tarefas independentes simultaneamente com async let.
O próximo artigo trata do núcleo desta série: actor. Ele explica exatamente o que é uma condição de corrida de dados, como actor a transforma em um conceito de tempo de compilação e a famosa armadilha de reentrancy.

![Imagem de capa de [Swift avançado #1] Como async/await funciona: não é a thread que para](/assets/images/posts/17c93232-c3fd-4d3b-867a-e93af7bc893f/swift-async-await-suspension-1.jpg)