Resta uma última pergunta para encerrar a série sobre Concurrency. Já vimos funções async (parte 1), actor (parte 2) e Sendable (parte 3).
Este conteúdo continua o artigo anterior Swift avançado #3.
Mas ainda não tratamos direito da porta de entrada para esse mundo assíncrono: Task. E, ao lidar com Task, surge o conceito mais prático de toda a série.
É a concorrência estruturada (structured concurrency).
O nome parece grandioso, mas a pergunta é simples: depois de iniciar uma tarefa assíncrona, quem é responsável por garantir que ela termine?
O que significa «estruturada»: tarefas também têm escopo
O nome vem do antigo termo programação estruturada. Depois da época em que goto podia lançar o fluxo de controle para qualquer lugar, os blocos if e for passaram a garantir que «o controle retorna ao ponto de onde saiu».
A concorrência estruturada aplica o mesmo princípio às tarefas assíncronas. Uma tarefa filha não pode sair do escopo do pai, e o pai só termina quando todas as filhas terminam.
O async let que vimos na parte 1 foi, na verdade, o primeiro exemplo desse princípio.
func loadDashboard() async throws -> Dashboard {
async let profile = fetchProfile() // tarefa filha 1
async let feed = fetchFeed() // tarefa filha 2
return try await Dashboard(profile: profile, feed: feed)
} // esta função não pode retornar antes que as duas tarefas filhas sejam limpas
As filhas criadas com async let precisam ser concluídas ou canceladas antes que a função retorne. O mesmo vale para exceções.
Se feed lançar um erro, profile, que ainda está em execução, receberá automaticamente o sinal de cancelamento. A função propaga o erro depois de concluir a limpeza.
Como as tarefas estão vinculadas ao escopo, não pode existir estruturalmente uma «tarefa iniciada e esquecida».
Na seção sobre closures, escaping era o rótulo de alerta para uma «closure que vive mais que a função». A concorrência estruturada remove por padrão as «tarefas que vivem mais que a função».
Quando o número de filhas é definido dinamicamente, usamos TaskGroup. Este é o código típico para receber uma lista de URL em paralelo.
let images = try await withThrowingTaskGroup(of: (URL, Image).self) { group in
for url in urls {
group.addTask { (url, try await download(url)) }
}
var result: [URL: Image] = [:]
for try await (url, image) in group {
result[url] = image
}
return result
}
Há dois pontos importantes. Os resultados chegam na ordem de conclusão (se a ordem precisar ser preservada, armazene-os com a chave, como acima), e todas as filhas do grupo são limpas quando a closure termina.
Se async let é a sintaxe para «algumas filhas», TaskGroup é a sintaxe para «n filhas»; a garantia de escopo é a mesma.
Cancelamento: o sinal chega, mas parar é responsabilidade sua
O segundo eixo da concorrência estruturada é a propagação do cancelamento. Quando o pai é cancelado, o sinal desce pela árvore até todos os descendentes.
Quando você sai de uma tela, as requisições de rede iniciadas por ela são canceladas em cadeia graças a essa estrutura.
Mas o cancelamento do Swift é cooperativo. O sinal de cancelamento não encerra a tarefa à força.
Ele apenas ativa o sinalizador isCancelled; verificar esse sinalizador e parar é responsabilidade da própria tarefa.
func processLargeFile() async throws {
for chunk in chunks {
try Task.checkCancellation() // se foi cancelada CancellationErrorlança um erro
await process(chunk)
}
}
Por que não forçar o encerramento? Porque, se uma tarefa morrer enquanto grava um arquivo pela metade, mantém um lock ou está no meio de uma transação, o sistema ficará inconsistente.
A lógica do cancelamento cooperativo é: «só a própria tarefa sabe qual é um bom ponto para parar».
A implicação prática é clara: loops longos devem incluir checkCancellation.
As API do sistema, como URLSession, já respeitam o cancelamento internamente, então você pode confiar nelas.
Por outro lado, um cálculo longo que ignora o cancelamento continuará executando mesmo depois de cancelado. Não é que o cancelamento falhe: ele não foi verificado.
Task e Task.detached: o mundo não estruturado e seu custo
Se até aqui estávamos no mundo estruturado, Task { } fica fora dele. Uma tarefa criada com Task é não estruturada (unstructured): ela não entra na árvore pai-filho.
Ela não fica vinculada a um escopo, e erros e cancelamento também não são propagados automaticamente.
Então por que ela existe? Porque precisamos de uma ponte do mundo síncrono para o assíncrono.
Um handler de toque de botão é uma função síncrona e não pode usar await, então Task { await viewModel.refresh() }abre um novo contexto assíncrono. Esse é o lugar legítimo de Task.
O modificador .task do SwiftUI acrescenta gerenciamento do ciclo de vida a essa ponte (cancelamento automático quando a view desaparece), por isso ele tem prioridade na UI.
O problema surge quando Task vira hábito: tarefas abertas dentro de funções async ou lançadas como fire-and-forget. Você precisa recuperar manualmente todas as garantias da estrutura (aguardar a conclusão, propagar erros e encadear cancelamentos).
É preciso guardar referências, chamar cancel diretamente e registrar os erros por conta própria. Sem esse código de gerenciamento, surgem erros que desaparecem silenciosamente e tarefas zumbis.
Como regra: dentro de um contexto async, async let e TaskGroup são o padrão; Task deve ser usado apenas na fronteira síncrono→assíncrono.
Task.detached está um nível mais distante. Não herda prioridade, isolamento de actor nem valores task-local: é uma tarefa completamente órfã (SE-0304).
Às vezes é usado para «fazer trabalho pesado fora do contexto de @MainActor», mas quase sempre é uma solução equivocada. Basta declarar esse trabalho como uma função async nonisolated: ela será executada automaticamente no pool cooperativo (parte 1).
Os casos realmente necessários para detached são raros: tarefas em segundo plano que precisam ser intencionalmente independentes do contexto atual. Nas palavras da documentação oficial, é o último recurso (last resort).
Síntese da série: quatro conceitos que formam uma única imagem
Para encerrar a série sobre Concurrency, vou resumir todo o panorama de uma vez.
async/await trouxe o fluxo assíncrono de volta ao campo de visão do compilador (parte 1). actor incorporou proteção serial ao estado mutável compartilhado (parte 2), e Sendable verifica a segurança dos valores que atravessam limites de isolamento (parte 3).
A concorrência estruturada vinculou o ciclo de vida das tarefas ao escopo. O que começa precisa terminar, e o cancelamento flui pela árvore (nesta parte).
Uma frase atravessa os quatro conceitos: transformar a disciplina implícita da concorrência em estruturas explícitas da linguagem.
A disciplina de gerenciar threads virou um pool cooperativo e suspension; a de gerenciar locks virou actor. O conhecimento informal sobre «posso passar este objeto entre threads?» virou Sendable, e o comentário de revisão «não esqueça de limpar a requisição» virou uma árvore de tarefas.
O princípio de segurança em primeiro lugar, visto na parte 1 da série de filosofia do Swift, foi ampliado para a área mais difícil: a concorrência. Essa é a narrativa completa do Swift Concurrency.
Resumo
- A concorrência estruturada estabelece que «tarefas filhas não podem sair do escopo pai». async let (quantidade fixa) e TaskGroup (quantidade dinâmica) são sua sintaxe; em caso de erro, o cancelamento das irmãs e a limpeza são automáticos.
- O cancelamento é cooperativo. O sinal se propaga pela árvore, mas quem para é a própria tarefa que inclui checkCancellation.
- Task { }é usado somente como ponte síncrono→assíncrono. O uso habitual de Task dentro de um contexto async transforma as garantias estruturais em dívida de gerenciamento manual. Task.detached é o último recurso.
- A ideia central da série: a disciplina implícita de gerenciar threads, locks e ciclos de vida foi transferida para as estruturas da linguagem suspension, actor, Sendable e árvore de tarefas.
A partir da próxima parte, vamos às profundezas do desempenho. Trataremos de por que final é uma palavra-chave de desempenho, onde as chamadas de protocolo ficam lentas e o que realmente são despacho estático e despacho dinâmico.
Continue lendo
Fontes e verificação
- SE-0304: Structured ConcurrencySwift Evolution · Padrão ou especificação · Consultado 17 de agosto de 2026Evidência: Estrutura pai-filho de Task e TaskGroup; herança de cancelamento, prioridade, valores task-local e contexto de actor

![Imagem de capa de [Swift avançado #4] Concorrência estruturada: por que evitar Task](/assets/images/posts/ab72c2de-7b69-438f-82b8-c4c8ba431871/swift-structured-concurrency-1.jpg)