Em abril de 2018, o desenvolvedor Nathaniel J. Smith — então
mantenedor da biblioteca Trio para concorrência em Python —
publicou no blog pessoal um ensaio com título provocativo:
Notes on Structured Concurrency, or: Go statement considered
harmful. O paralelo com o famoso Goto Statement Considered
Harmful que Edsger Dijkstra publicara em 1968 era deliberado.
Smith argumentava que a primitiva go de Go — e os
equivalentes em outras linguagens (asyncio.create_task,
Thread.start, setTimeout) — sofrem dos
mesmos defeitos estruturais que o goto: permitem
criar fluxos de controle que pulam para fora do escopo léxico,
tornando difícil ou impossível raciocinar sobre quando uma
operação termina, onde os erros aparecem, e como cancelar
operações pendentes.
O paralelo provocou desconforto. Goroutines são uma das primitivas
mais celebradas da última década; chamá-las de "harmful" parecia
heresia. Mas o argumento de Smith era preciso: não é a
concorrência que está errada — é a forma como ela é
exposta sintaticamente. Um go func() { ... }() em Go
produz uma goroutine que vive fora do escopo da função que a
criou. Se essa função retorna, a goroutine continua. Se a goroutine
crasha, o pânico fica perdido a menos que alguém esteja
explicitamente recuperando. Se você quer cancelar a goroutine,
precisa passar um context.Context manualmente e
esperar que toda a stack de chamadas honre o cancelamento.
Tudo isso é resolvível, mas exige disciplina; o sistema de tipos
e a estrutura léxica não ajudam.
O ensaio defendia uma alternativa elegante: amarrar concorrência
ao escopo léxico, da mesma forma que blocos
{ } e funções amarram fluxos de controle síncronos.
Você abre um nursery (Trio chamou assim, hoje a maioria
diz "task group" ou "task scope"); dentro dele você inicia
tarefas concorrentes; quando o nursery fecha, garante-se que
todas as tarefas iniciadas terminaram, com sucesso ou
com erro. Erros propagam para fora do nursery como exceções
normais. Cancelamento de uma propaga para todas. Ninguém vaza.
Em 2026, structured concurrency é mainstream. Python asyncio
ganhou TaskGroup em 3.11 (2022); Java Loom trouxe
StructuredTaskScope em JDK 21 (2023); Kotlin
coroutines têm structured concurrency embutida por design desde
o lançamento estável (2018); Swift 5.5 (2021) introduziu
async let e withTaskGroup. Em Go, o
pacote errgroup de
golang.org/x/sync oferece estrutura semelhante. C#
tem suporte parcial via Task.WhenAll e
Parallel.ForEachAsync. Esse conceito explora o que
cada um entrega e por que vale adotar.
O que é "estruturado" — o paralelo com o goto
Antes de Dijkstra, a maioria das linguagens de programação tinha
goto como primitiva primária de controle. Você
podia pular de qualquer lugar para qualquer outro, e o programa
virava um emaranhado difícil de seguir. A revolução da
programação estruturada (Böhm-Jacopini 1966, Dijkstra 1968) foi
mostrar que blocos aninhados, condicionais (if/else)
e laços (while/for) bastam para expressar qualquer
computação — e que código com essas estruturas é fundamentalmente
mais fácil de raciocinar. Hoje, goto existe em
poucas linguagens e é raramente usado mesmo onde existe.
Smith argumenta que a concorrência viveu por décadas no estado
pré-estruturado. go func() {...}() em Go, como
asyncio.create_task(coro) em Python, ou
new Thread(...).start() em Java, são equivalentes a
goto: criam um fluxo de execução que pula para fora
do escopo léxico atual e continua de forma independente. Como
goto, é poderoso. Como goto, gera código difícil de seguir. A
pergunta "todas as tarefas iniciadas nesta função terminaram
quando a função retorna?" não tem resposta sem inspecionar o
código todo — equivalente a "este programa termina aqui?" em
mundo com goto.
Structured concurrency aplica o mesmo princípio: tarefas concorrentes vivem dentro de blocos léxicos. Quando o bloco termina, todas as tarefas iniciadas dentro dele terminaram. Não tem mais "task que continua depois da função pai retornar". A estrutura léxica passa a refletir a estrutura de execução concorrente.
# Não-estruturado (Python pré-3.11 idiomático)
async def consultar_tudo():
task_a = asyncio.create_task(api_a.consultar())
task_b = asyncio.create_task(api_b.consultar())
# Se exceção acontece aqui, task_a e task_b ficam órfãs.
# Se a função retorna sem aguardar, idem.
return await task_a, await task_b
# Estruturado (Python 3.11+)
async def consultar_tudo():
async with asyncio.TaskGroup() as tg:
task_a = tg.create_task(api_a.consultar())
task_b = tg.create_task(api_b.consultar())
# Saiu do bloco: garantido que ambas terminaram.
# Exceção em qualquer uma cancela a outra e propaga ExceptionGroup.
return task_a.result(), task_b.result()
A diferença sintática é pequena. A diferença semântica é grande.
No segundo, o bloco async with garante por
construção que todas as tarefas terminaram antes de você ler os
resultados — você não precisa lembrar de fazer a limpeza
manualmente. Erros não somem. Cancelamento se propaga.
O que se ganha — quatro propriedades essenciais
Structured concurrency entrega quatro propriedades que sistemas não-estruturados precisam reconstruir caso a caso. Vale conhecer cada uma porque elas são exatamente os pontos onde sistemas maduros costumam falhar.
1. Escopo léxico: tarefas vivem em blocos
A função pai não retorna até todas as tarefas filhas terminarem.
Se você vê código que abre um TaskGroup em uma
função, você sabe — sem inspecionar mais nada — que quando
essa função sair, nada concorrente que ela disparou continuará
pendente. Para revisar código concorrente, isso é
transformacional. Você pode raciocinar localmente.
2. Erros que não somem
Em modelo não-estruturado, quando uma task dispara exceção, a
exceção fica armazenada no objeto Task; se ninguém faz
await nela, a exceção é silenciosamente descartada
(com warning, no melhor caso). Em structured, qualquer exceção
em qualquer task é propagada para fora do bloco — você
não pode esquecer. Em Python 3.11+,
ExceptionGroup agrega múltiplas exceções de tarefas
que falharam em paralelo. Em Java Loom,
StructuredTaskScope oferece políticas
configuráveis (ShutdownOnFailure,
ShutdownOnSuccess) que decidem o comportamento
coletivo.
3. Cancelamento que propaga
Cancelamento é tema de seu próprio conceito (13), mas é central em structured concurrency. Cancelar o escopo cancela automaticamente todas as tarefas filhas. Cancelar uma tarefa filha pode (dependendo da política) cancelar suas irmãs, ou só ela mesma. A propagação hierárquica imita escopo léxico: tudo que existe dentro do bloco é cancelado quando o bloco é cancelado. Sem structured, propagação de cancelamento é trabalho manual repetido em cada estrutura concorrente.
4. Resources e cleanup
Tarefas concorrentes frequentemente usam resources (conexões,
arquivos, locks). Em modelo não-estruturado, garantir cleanup
em toda a árvore de tarefas é proverbialmente difícil — uma
task que falha pode deixar resource aberto que só será limpo na
garbage collection. Structured, ao garantir terminação no fim
do bloco, simplifica enormemente o cleanup: async with
do recurso pode envolver o async with do task group,
e a ordem de fechamento fica óbvia.
Estrutura léxica deve refletir estrutura de execução. Se você vê uma função, espera que tudo que ela inicia termine antes dela retornar. Structured concurrency é simplesmente a aplicação consistente desse princípio à concorrência.
Trio — a referência canônica em Python
A biblioteca Trio, criada por Smith em 2017, antes do
TaskGroup entrar no asyncio padrão, é a implementação
mais limpa e didática de structured concurrency em uso. Vale
olhar mesmo que você nunca vá usá-la diretamente.
import trio
async def buscar_url(url):
print(f"buscando {url}")
# ... lógica real
return f"resultado de {url}"
async def buscar_todas(urls):
resultados = {}
# 'nursery' é o termo do Trio para task scope.
async with trio.open_nursery() as nursery:
async def buscar_e_guardar(u):
resultados[u] = await buscar_url(u)
for url in urls:
nursery.start_soon(buscar_e_guardar, url)
# Aqui, garantido: todas as tarefas terminaram.
# Se qualquer uma falhou, ExceptionGroup foi levantado e nem chegamos aqui.
return resultados
trio.run(buscar_todas, ["a.com", "b.com", "c.com"])
Note duas escolhas de design. Primeira: open_nursery()
é um context manager — sintaxe igual a abrir arquivo. Segunda:
start_soon não retorna a task; o uso normal é
"dispare e esqueça, dentro do nursery". Você pode pegar
referência se precisar, mas o caminho fácil é o estruturado.
Smith argumentou que API design importa: tornar o caminho
estruturado o mais fácil força adoção do padrão correto.
O asyncio.TaskGroup da stdlib seguiu de perto o
modelo de Trio quando entrou em Python 3.11 (PEP 654 introduziu
ExceptionGroup para suportar múltiplos erros
agregados). Em código novo de 2022 em diante, vale preferir
TaskGroup sobre asyncio.gather sempre
que possível — as garantias estruturadas valem o pequeno custo
sintático.
Java Loom — structured concurrency para a JVM
Java passou décadas com a API de threads tradicional (1:1, kernel
threads), e qualquer concorrência interessante precisava de
ExecutorService com cuidado manual. Em junho de
2023, JDK 21 lançou virtual threads (Project Loom) e — junto
— uma API preview de structured concurrency:
StructuredTaskScope. Em 2026, a API ainda
é preview, mas amplamente adotada em projetos
novos modernos.
// Java 21+ (preview API)
import java.util.concurrent.StructuredTaskScope;
Resultado consultarTudo() throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var tarefaA = scope.fork(() -> apiA.consultar());
var tarefaB = scope.fork(() -> apiB.consultar());
scope.join(); // espera todas
scope.throwIfFailed(); // propaga primeira falha
return new Resultado(tarefaA.get(), tarefaB.get());
}
// try-with-resources fecha o scope:
// garante que todas as tarefas terminaram (cancelando se necessário).
}
Java oferece duas políticas built-in. ShutdownOnFailure:
se qualquer tarefa falha, todas as outras são canceladas
imediatamente — apropriado para "preciso de todos os resultados,
não vale a pena continuar se um falhou". ShutdownOnSuccess:
retorna assim que qualquer tarefa termina com sucesso, cancela
as outras — apropriado para "qualquer um destes serviços serve;
vou com o primeiro que responder". Você pode estender criando
políticas próprias.
Combinado com virtual threads, structured concurrency em Loom é
especialmente potente. scope.fork(...) cria uma
virtual thread (custo trivial); cancelamento se propaga
hierarquicamente; e o limite prático de tarefas concorrentes
sobe para milhões. JEP 453 detalha a motivação e o design.
Go errgroup — quase estruturado
Go não tem structured concurrency como primitiva sintática, mas
o pacote golang.org/x/sync/errgroup implementa
uma versão pragmática que cobre a maioria dos casos. É o padrão
idiomático em Go moderno para qualquer código com mais de uma
goroutine relacionada.
import "golang.org/x/sync/errgroup"
func consultarTudo(ctx context.Context) (Resultado, error) {
g, ctx := errgroup.WithContext(ctx)
var ra, rb Resposta
g.Go(func() error {
var err error
ra, err = apiA.Consultar(ctx)
return err
})
g.Go(func() error {
var err error
rb, err = apiB.Consultar(ctx)
return err
})
if err := g.Wait(); err != nil {
return Resultado{}, err
}
return Resultado{ra, rb}, nil
}
Algumas observações importantes. errgroup.WithContext
cria um context filho que é cancelado quando qualquer goroutine
retorna erro — o cancelamento se propaga para as outras
goroutines via ctx que elas devem honrar.
g.Wait() espera todas e retorna o primeiro erro
observado. A estrutura é "estruturada por convenção": o pattern
é claro, mas o compilador não impede que você inicie goroutine
fora do g e cause vazamento. Em Java, Python e
Trio, o sistema de tipos faz parte do trabalho; em Go, fica por
conta da disciplina.
Kotlin — structured concurrency by design
Kotlin coroutines, lançadas estáveis em 2018, foram desenhadas
desde o início com structured concurrency como princípio
central — em larga medida pela influência do trabalho de Smith.
Em Kotlin, toda coroutine vive em um CoroutineScope;
cancelar o scope cancela tudo dentro. Isso aparece em frameworks
como Android (cada Activity tem seu scope, ciclo de vida limpo
automático) e Spring (controllers têm scope da request).
// Kotlin
suspend fun consultarTudo(): Resultado = coroutineScope {
val tarefaA = async { apiA.consultar() }
val tarefaB = async { apiB.consultar() }
Resultado(tarefaA.await(), tarefaB.await())
}
// coroutineScope { ... } só retorna após todas as filhas terminarem.
// Se qualquer uma lançar, as outras são canceladas, exceção propaga.
Kotlin foi pioneira em fazer structured concurrency uma feature central, não um add-on. Para entender o estilo idiomático moderno em concorrência, vale estudar Kotlin coroutines mesmo sem usar a linguagem.
Por que importa: tasks órfãs e bugs invisíveis
A motivação concreta de Smith para structured concurrency veio de bugs reais em sistemas Python/Trio. O padrão recorrente: uma task é iniciada e ninguém aguarda; a exceção dela some no log; o sistema "não funciona" sem mensagem clara. Em servidores web, isso aparece como request que silenciosamente não atualiza um campo; em scrapers, como item que silenciosamente não é processado; em pipelines, como dado que silenciosamente não chega ao destino.
O caso clássico em asyncio pre-3.11:
async def processar(item):
asyncio.create_task(persistir(item)) # fire-and-forget
asyncio.create_task(notificar(item)) # fire-and-forget
return "ok"
# Se persistir() ou notificar() lançar, o erro vai para o log
# como "Task exception was never retrieved" — fácil de ignorar.
# Pior: se você fizer o GC limpar a task antes dela rodar, ela some.
A versão estruturada elimina o bug por construção:
async def processar(item):
async with asyncio.TaskGroup() as tg:
tg.create_task(persistir(item))
tg.create_task(notificar(item))
# Aqui ambas terminaram. Se qualquer uma falhou, ExceptionGroup.
return "ok"
Para sistemas em produção, a diferença não é estética. É a diferença entre erros que aparecem e erros que somem.
O mesmo padrão nas três linguagens
Para concretizar, considere um padrão comum: chamar três APIs em paralelo, agregar resultados, falhar se qualquer uma falhar. Veja como cada linguagem expressa isso de forma estruturada.
using System.Threading;
using System.Threading.Tasks;
async Task<Resultado> ConsultarTudoAsync(CancellationToken ct = default) {
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
Task<Resposta> tarefaA = ApiA.ConsultarAsync(cts.Token);
Task<Resposta> tarefaB = ApiB.ConsultarAsync(cts.Token);
Task<Resposta> tarefaC = ApiC.ConsultarAsync(cts.Token);
try {
await Task.WhenAll(tarefaA, tarefaB, tarefaC);
} catch {
cts.Cancel(); // cancela as outras se uma falhar
throw;
}
return new Resultado(
await tarefaA, await tarefaB, await tarefaC);
}
C# não tem ainda primitiva de structured concurrency formal;
Task.WhenAll + CancellationTokenSource
aproximam o padrão. Parallel.ForEachAsync
(.NET 6+) é uma forma mais estruturada para coleções. Existe
discussão sobre adicionar API explícita em versões futuras
do .NET, ainda sem deadline público em 2026.
import asyncio
async def consultar_tudo() -> Resultado:
async with asyncio.TaskGroup() as tg:
ta = tg.create_task(api_a.consultar())
tb = tg.create_task(api_b.consultar())
tc = tg.create_task(api_c.consultar())
# Saiu do bloco: todas terminaram.
# Se qualquer uma falhou, ExceptionGroup foi levantado.
return Resultado(ta.result(), tb.result(), tc.result())
# Captura: try/except* para ExceptionGroup
try:
res = asyncio.run(consultar_tudo())
except* ConnectionError as eg:
for e in eg.exceptions:
print(f"erro de conexão: {e}")
TaskGroup é a primitiva idiomática em Python
moderno. Dois detalhes importantes: (1) try/except*
(Python 3.11+) é a sintaxe para capturar exceções específicas
dentro de um ExceptionGroup; (2) cancelamento
propaga automaticamente — se api_a levantar,
api_b e api_c são canceladas
imediatamente.
package main
import (
"context"
"golang.org/x/sync/errgroup"
)
func consultarTudo(ctx context.Context) (Resultado, error) {
g, ctx := errgroup.WithContext(ctx)
var ra, rb, rc Resposta
g.Go(func() error {
var err error
ra, err = apiA.Consultar(ctx)
return err
})
g.Go(func() error {
var err error
rb, err = apiB.Consultar(ctx)
return err
})
g.Go(func() error {
var err error
rc, err = apiC.Consultar(ctx)
return err
})
if err := g.Wait(); err != nil {
return Resultado{}, err
}
return Resultado{ra, rb, rc}, nil
}
errgroup.WithContext dá um context que é
cancelado quando qualquer goroutine retorna erro — o
cancelamento se propaga para as outras desde que elas
honrem ctx. g.Wait() retorna o
primeiro erro. Convenção forte, sem garantia compilada;
uma goroutine iniciada fora do g não é
rastreada.
Quando structured concurrency atrapalha
Structured concurrency cobre 90% dos casos onde concorrência é útil. Mas há cenários onde a estrutura não cabe naturalmente — vale conhecê-los para não tentar forçar.
Tasks com vida útil maior que o caller
Worker que vive enquanto o servidor estiver de pé; pool de
conexões que sobrevive a múltiplas requests; tarefas em
background que executam periodicamente. Esses casos têm escopo
maior que qualquer função; o task scope correspondente fica no
nível da aplicação inteira. Em Python, isso vira um único
TaskGroup no main que envolve a vida do processo.
Em Go, idem — uma errgroup ao nível mais alto. A estrutura
ainda existe, só que é maior.
Coordenação dinâmica
Sistemas onde tarefas se criam mutuamente em tempo de execução
(workers que disparam workers, recursão dinâmica, fan-out
indeterminado) podem ficar awkward dentro de structured.
TaskGroup aceita criação dinâmica
(tg.create_task a qualquer hora dentro do bloco),
mas se a árvore for muito profunda ou mutável, a estrutura
léxica vai ficando difícil. Padrões: passar o
TaskGroup para baixo da call stack (Python aceita,
embora Trio recomende contra), ou usar canais para comunicação
em vez de criação dinâmica.
APIs callback-based legadas
Se você precisa integrar com uma API antiga que aceita callback
e não tem versão async/await ou Future-like, structured
concurrency não te ajuda diretamente. O caminho é envolver a
API em uma camada async (asyncio.Future manual,
TaskCompletionSource em C#) — depois de envolvida,
ela cabe no padrão estruturado.
Migrar de asyncio.gather para
asyncio.TaskGroup requer atenção: o
comportamento em caso de erro é diferente. gather
(com return_exceptions=False, default) cancela
as outras quando uma falha mas não as agrega corretamente em
log; TaskGroup levanta ExceptionGroup
que precisa ser capturado com except*. Código
antigo de captura de exceção pode silenciosamente deixar de
funcionar. Teste cobertura de erro depois de migrar.
O futuro — structured async em todo lugar
A direção da indústria é clara. Linguagens novas (Swift, Zig)
têm structured concurrency desde o design. Linguagens antigas
estão adicionando: Java em 2023, Python em 2022, .NET em
discussão. Mesmo Go, que historicamente resistiu a
"abstrações sobre goroutines", incorporou
errgroup no x/sync e tem propostas internas de
structured task groups discutidas em conferences.
A lição mais ampla é a clássica de Dijkstra: estrutura léxica
ajuda raciocínio; primitivas que furam essa estrutura precisam
de boa justificativa para sobreviver. Goroutine como
goto é provocação útil porque expõe que muito do
que aceitamos como "modelo de concorrência" é, no fundo, um
retrocesso para o pré-estruturado. Structured concurrency é
simplesmente a aplicação consistente da programação estruturada
ao domínio concorrente.
Como praticar
-
Migre código de
asyncio.gatherparaTaskGroup. Pegue um pedaço de código Python async que usegather. Reescreva comTaskGroup. Documente o que mudou em comportamento de erro. Force uma das tarefas a falhar e veja comoExceptionGroupaparece. Capture comexcept*. - Construa um rate limiter usando structured concurrency. Em qualquer linguagem com TaskGroup/errgroup. Limite de N requisições simultâneas; quando uma termina, próxima inicia. Garanta limpeza correta no fim. Bonus: adicione cancelamento externo (timeout do batch inteiro).
-
Compare comportamento de erro entre dois modelos.
Em Python, escreva o mesmo programa duas vezes: uma com
asyncio.create_tasksemawait(não-estruturado), outra comTaskGroup. Em ambas, faça uma das tarefas levantar exceção. Compare: o que aparece no log? O programa termina silenciosamente ou com erro claro? Documente o que isso implica para um sistema em produção.
Referências para aprofundar
- artigo Notes on Structured Concurrency, or: Go statement considered harmful — Nathaniel J. Smith (2018).
- artigo Timeouts and Cancellation for Humans — Nathaniel J. Smith (2018).
- artigo Coroutines and Structured Concurrency — Roman Elizarov (Kotlin Conf 2019).
- artigo Structured Concurrency in Java — Ron Pressler (2022).
- docs Trio — Tutorial & design rationale.
- docs Python — asyncio.TaskGroup.
- docs JEP 453 — Structured Concurrency (Preview).
- docs PEP 654 — Exception Groups and except*.
- docs Kotlin Coroutines — Structured Concurrency.
- docs errgroup package documentation.
- livro Programming Concurrency on the JVM — Venkat Subramaniam (2011, com revisões).
- vídeo Structured Concurrency — Java 21 — José Paumard.