TaskGroup no Python: concorrência estruturada

Publicado em: 28/08/2026
Tempo de leitura: 5 minutos
A detailed view of computer programming code on a screen, showcasing software development.

Criar várias tarefas com asyncio.create_task() é simples, mas coordenar o ciclo de vida delas pode ser difícil. Uma tarefa pode falhar enquanto outras continuam executando, referências podem ser perdidas e o encerramento pode acontecer antes do cleanup. asyncio.TaskGroup organiza tarefas assíncronas dentro de um bloco estruturado: ao sair do contexto, todas terminaram, foram canceladas ou tiveram seus erros reunidos.

Neste guia, você aprenderá a criar tarefas em grupo, coletar resultados, entender cancelamento, tratar ExceptionGroup, comparar TaskGroup com asyncio.gather(), aninhar grupos, aplicar timeouts e projetar serviços concorrentes previsíveis.

O problema das tarefas soltas

Uma tarefa criada com create_task() começa a executar independentemente. O chamador precisa guardar a referência, aguardar o resultado e lidar com falhas.

import asyncio

async def trabalho(nome: str) -> str:
    await asyncio.sleep(0.2)
    return nome.upper()

async def main():
    tarefas = [
        asyncio.create_task(trabalho("a")),
        asyncio.create_task(trabalho("b")),
    ]
    resultados = await asyncio.gather(*tarefas)
    print(resultados)

asyncio.run(main())

Esse padrão funciona, mas aumenta a chance de uma tarefa escapar do escopo lógico que a criou. TaskGroup torna o vínculo explícito.

Primeiro TaskGroup

import asyncio

async def trabalho(nome: str) -> str:
    await asyncio.sleep(0.2)
    return nome.upper()

async def main():
    async with asyncio.TaskGroup() as grupo:
        tarefa_a = grupo.create_task(trabalho("a"))
        tarefa_b = grupo.create_task(trabalho("b"))

    print(tarefa_a.result())
    print(tarefa_b.result())

asyncio.run(main())

O bloco só termina depois que todas as tarefas terminam. Após o async with, os resultados estão disponíveis e não existe trabalho pendente pertencente ao grupo.

Concorrência estruturada

Concorrência estruturada significa que tarefas filhas vivem dentro de um escopo claro. O código que cria as tarefas também é responsável por aguardar seu encerramento. Isso facilita raciocinar sobre recursos, cancelamento e erros.

O conceito complementa o guia de asyncio no Python. Corrotinas fornecem concorrência cooperativa; TaskGroup fornece organização para múltiplas corrotinas relacionadas.

Falha em uma tarefa

Quando uma tarefa do grupo falha com uma exceção diferente de cancelamento, TaskGroup normalmente cancela as outras tarefas ainda em execução. Depois que todas encerram, as falhas são levantadas como ExceptionGroup.

async def rapido():
    await asyncio.sleep(0.1)
    raise ValueError("dados inválidos")

async def lento():
    try:
        await asyncio.sleep(10)
    finally:
        print("cleanup do lento")

async def main():
    async with asyncio.TaskGroup() as grupo:
        grupo.create_task(rapido())
        grupo.create_task(lento())

A tarefa lenta recebe cancelamento, executa o bloco finally e o grupo só então propaga o conjunto de erros.

Tratando ExceptionGroup com except*

async def main():
    try:
        async with asyncio.TaskGroup() as grupo:
            grupo.create_task(rapido())
            grupo.create_task(lento())
    except* ValueError as grupo_erros:
        for erro in grupo_erros.exceptions:
            print("erro de validação:", erro)

except* seleciona exceções compatíveis dentro de um grupo. Outros tipos não tratados continuam sendo propagados. Essa abordagem será aprofundada no conjunto sobre ExceptionGroup.

Coletando resultados

TaskGroup.create_task() devolve um objeto Task. Guarde as referências quando precisar dos resultados individuais.

async def consultar(id: int) -> dict:
    await asyncio.sleep(0.1)
    return {"id": id}

async def carregar(ids: list[int]) -> list[dict]:
    tarefas: list[asyncio.Task[dict]] = []
    async with asyncio.TaskGroup() as grupo:
        for id in ids:
            tarefas.append(grupo.create_task(consultar(id)))

    return [tarefa.result() for tarefa in tarefas]

A ordem das tarefas na lista preserva a ordem de criação, mesmo que terminem em momentos diferentes.

TaskGroup versus gather

asyncio.gather() continua útil quando você já possui awaitables e deseja um único resultado ordenado. Com return_exceptions=True, ele pode coletar falhas como valores. TaskGroup, porém, oferece um modelo mais estruturado para criar tarefas dentro de um escopo e cancelar irmãs quando uma falha.

Não substitua mecanicamente todo gather(). Escolha TaskGroup para unidades de trabalho que devem viver e morrer juntas; use gather quando seu comportamento de agregação for exatamente o desejado.

Adicionando tarefas dinamicamente

Enquanto o grupo estiver ativo, uma corrotina pode receber o próprio grupo e adicionar tarefas.

async def descobrir(grupo: asyncio.TaskGroup, pagina: int) -> None:
    await asyncio.sleep(0.1)
    if pagina < 3:
        grupo.create_task(descobrir(grupo, pagina + 1))

async def main():
    async with asyncio.TaskGroup() as grupo:
        grupo.create_task(descobrir(grupo, 1))

Novas tarefas só podem ser adicionadas enquanto o contexto está aberto. Evite crescimento sem limites: imponha profundidade, capacidade ou filas.

Cancelamento externo

Se a tarefa que contém o TaskGroup for cancelada, o grupo cancela suas tarefas filhas e aguarda o encerramento. Corrotinas devem respeitar CancelledError e liberar recursos em finally.

async def consumidor():
    recurso = await abrir_recurso()
    try:
        await processar(recurso)
    finally:
        await recurso.fechar()

Não capture CancelledError e continue indefinidamente. Se precisar executar cleanup, finalize e propague o cancelamento.

Não engolir CancelledError

TaskGroup e outras ferramentas de asyncio usam cancelamento internamente. Uma corrotina que captura BaseException ou CancelledError sem relançar pode quebrar o comportamento estruturado.

async def incorreto():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        return  # pode esconder o cancelamento

O padrão seguro é limpar e usar raise, salvo quando existe uma razão cuidadosamente documentada para consumir o cancelamento.

Timeout envolvendo o grupo

Use asyncio.timeout() para impor um prazo ao escopo inteiro.

async def main():
    try:
        async with asyncio.timeout(2.0):
            async with asyncio.TaskGroup() as grupo:
                grupo.create_task(operacao_a())
                grupo.create_task(operacao_b())
    except TimeoutError:
        print("prazo excedido")

Ao expirar, a tarefa externa é cancelada; o TaskGroup cancela e aguarda as filhas. O próximo artigo do lote detalha o gerenciador de timeout.

Timeout individual por tarefa

Quando cada operação possui um prazo próprio, coloque o timeout dentro da corrotina filha.

async def com_prazo(coro, segundos: float):
    async with asyncio.timeout(segundos):
        return await coro

Uma expiração individual gera TimeoutError naquela tarefa e, por padrão, causa cancelamento das outras tarefas do grupo. Se a falha for recuperável, trate-a dentro da filha e retorne um resultado explícito.

Falhas esperadas como dados

Nem todo erro deve derrubar o grupo. Em um processamento em lote, talvez um item inválido deva produzir um resultado de falha enquanto os demais continuam.

from dataclasses import dataclass

@dataclass
class Resultado:
    id: int
    valor: str | None = None
    erro: str | None = None

async def processar_seguro(id: int) -> Resultado:
    try:
        valor = await consultar_texto(id)
        return Resultado(id=id, valor=valor)
    except ErroEsperado as exc:
        return Resultado(id=id, erro=str(exc))

Reserve exceções não tratadas para condições que realmente invalidam a unidade concorrente.

Grupos aninhados

TaskGroups podem ser aninhados para representar suboperações.

async def processar_cliente(cliente_id: int):
    async with asyncio.TaskGroup() as grupo:
        grupo.create_task(carregar_perfil(cliente_id))
        grupo.create_task(carregar_pedidos(cliente_id))

async def main(ids: list[int]):
    async with asyncio.TaskGroup() as grupo:
        for id in ids:
            grupo.create_task(processar_cliente(id))

Cada nível controla suas próprias filhas. Falhas podem formar ExceptionGroups aninhados, preservando a estrutura do trabalho.

Limitar concorrência

TaskGroup não limita automaticamente a quantidade de tarefas simultâneas. Criar cem mil tarefas pode consumir muita memória. Use asyncio.Semaphore, uma fila com workers ou lotes.

limite = asyncio.Semaphore(20)

async def limitado(item):
    async with limite:
        return await processar(item)

O grupo organiza o ciclo de vida; o semáforo controla capacidade.

Context variables e nomes de tarefas

create_task() aceita recursos como nome da tarefa e contexto, conforme a versão do Python. Nomes ajudam no diagnóstico.

grupo.create_task(
    consultar(42),
    name="consulta-cliente-42",
)

Context variables normalmente são copiadas para a nova tarefa, permitindo request IDs e tracing por operação.

Erros comuns

  • Criar tarefas fora do grupo sem necessidade: elas podem escapar do ciclo de vida.
  • Engolir CancelledError: isso interfere no cancelamento estruturado.
  • Esperar que TaskGroup retorne uma lista: guarde as Tasks para obter resultados.
  • Criar tarefas sem limite: use semáforos, filas ou lotes.
  • Tratar todas as falhas como exceções fatais: erros esperados podem virar dados.
  • Ignorar cleanup: use finally e context managers assíncronos.

Exemplo completo: agregador de serviços

import asyncio

async def obter_usuario(id: int) -> dict:
    await asyncio.sleep(0.1)
    return {"id": id, "nome": "Ana"}

async def obter_pedidos(id: int) -> list[dict]:
    await asyncio.sleep(0.2)
    return [{"id": 1, "total": 99.0}]

async def montar_painel(id: int) -> dict:
    async with asyncio.timeout(3.0):
        async with asyncio.TaskGroup() as grupo:
            usuario = grupo.create_task(obter_usuario(id), name="usuario")
            pedidos = grupo.create_task(obter_pedidos(id), name="pedidos")

    return {
        "usuario": usuario.result(),
        "pedidos": pedidos.result(),
    }

O painel só é retornado depois que ambas as operações terminam. Uma falha cancela a operação irmã e evita retornar um estado parcial não planejado.

Conclusão

asyncio.TaskGroup organiza tarefas relacionadas em um escopo com início e fim claros. Ele aguarda todas as filhas, coordena cancelamento e reúne falhas, tornando o código assíncrono mais previsível.

A documentação oficial de TaskGroup no asyncio detalha propagação de exceções e cancelamento. Use grupos para operações que devem viver juntas, limite concorrência separadamente e trate cleanup e erros esperados de forma explícita.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Código em tela representando t-strings no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    T-strings no Python 3.14: templates estruturados

    Aprenda t-strings no Python 3.14, entenda Template e Interpolation e crie renderizadores estruturados, extensíveis e mais seguros.

    Ler mais

    Tempo de leitura: 5 minutos
    16/10/2026
    CPU e módulos de memória representando shared_memory track no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    shared_memory track: controle o resource tracker

    Aprenda o parâmetro track de multiprocessing.shared_memory no Python para controlar o resource tracker e evitar remoções prematuras.

    Ler mais

    Tempo de leitura: 6 minutos
    16/10/2026
    Unidade de armazenamento de dados representando compactação com compression.zstd no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: streams e compressão segura

    Aprenda compression.zstd no Python para compactar arquivos e dados com velocidade, streaming, dicionários e segurança.

    Ler mais

    Tempo de leitura: 6 minutos
    15/10/2026
    Código em tela para diagnóstico de processos com sys.remote_exec no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    sys.remote_exec: diagnostique processos Python

    Aprenda sys.remote_exec no Python para diagnosticar processos em execução com scripts controlados, segurança e boas práticas.

    Ler mais

    Tempo de leitura: 6 minutos
    15/10/2026
    Código Python e configuração de variáveis de ambiente com os.reload_environ
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    os.reload_environ: riscos com threads e ambiente

    Aprenda os.reload_environ no Python para atualizar variáveis de ambiente alteradas externamente, validar configurações e evitar problemas com threads.

    Ler mais

    Tempo de leitura: 5 minutos
    14/10/2026
    Código Python representando contextvars.Token e isolamento de contexto assíncrono
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    contextvars.Token: restaure contexto async

    Aprenda contextvars.Token no Python para definir valores temporários, restaurar contextos e isolar dados entre tarefas assíncronas.

    Ler mais

    Tempo de leitura: 6 minutos
    14/10/2026