Concorrência Moderna: async/await, Event Loop e Coroutines no Python
Concorrência em Python é o conjunto de técnicas para progredir em mais de uma tarefa ao mesmo tempo, especialmente quando o programa passa boa parte do tempo esperando alguma coisa: rede, disco, banco de dados, fila, API externa. Em aplicações modernas — web, automação, coleta de dados, integrações e bots — esse “tempo de espera” costuma ser o maior gargalo.
Nos últimos anos, o ecossistema Python consolidou um modelo de concorrência baseado em coroutines, async/await e um event loop (principalmente via asyncio). O resultado é um código que pode lidar com milhares de conexões e operações de I/O com eficiência, desde que usado no cenário certo.
Este artigo explica, de forma prática e baseada em fatos, como esses conceitos se conectam e quando vale a pena adotá-los.
O que “concorrência” significa (e o que ela não significa)
Antes de entrar em async/await, é importante separar três ideias que costumam ser confundidas:
- Concorrência: várias tarefas fazem progresso em períodos sobrepostos. Pode acontecer em um único núcleo de CPU, alternando execução, ou em múltiplos núcleos.
- Paralelismo: várias tarefas executam literalmente ao mesmo tempo, tipicamente em múltiplos núcleos (processos) ou múltiplas threads com trabalho CPU-bound.
- Assíncrono (async): um estilo de programação em que você inicia uma operação e não bloqueia enquanto espera a resposta. Em Python, isso normalmente é usado para I/O-bound (rede, disco, sockets).
Em termos práticos: asyncio é uma das ferramentas mais relevantes para Concorrência em Python, mas não é uma solução mágica para acelerar qualquer tipo de processamento.
Por que async/await ficou central no Python moderno
A motivação do modelo assíncrono é simples: bloqueio custa caro. Um servidor web tradicional que cria uma thread por conexão pode ficar limitado por memória, contenção e overhead de agendamento. Já um loop de eventos consegue administrar muitas conexões ativas com poucos recursos, desde que as tarefas cooperem.
No Python, o estilo async/await tornou a escrita de código assíncrono mais legível do que abordagens antigas baseadas em callbacks. Em vez de “encadear funções”, você escreve um fluxo quase linear, mas com pontos explícitos onde a tarefa “cede” o controle.
Coroutines: a unidade básica do async em Python
Uma coroutine é uma função declarada com async def. Ao ser chamada, ela não executa imediatamente como uma função comum: ela retorna um objeto coroutine que precisa ser agendado/awaited.
Exemplo mínimo:
import asyncio
async def tarefa():
await asyncio.sleep(1)
return "ok"
async def main():
resultado = await tarefa()
print(resultado)
asyncio.run(main())
Pontos importantes:
async defdefine uma coroutine.awaitmarca um ponto onde a coroutine pode pausar e permitir que outra tarefa rode.asyncio.run()inicia o event loop, executamain()e encerra o loop ao final.
Sem await (ou sem agendamento explícito), a coroutine não progride.
Event Loop: o “motor” da concorrência assíncrona
O event loop é um laço que:
- Mantém uma fila de tarefas (coroutines) prontas para rodar.
- Observa eventos de I/O (ex.: socket pronto para leitura, timer expirado).
- Retoma a execução das coroutines que estavam aguardando (
await) esses eventos.
A ideia central é cooperativa: em vez de o sistema interromper a tarefa a qualquer momento (preempção, como em threads), a coroutine devolve o controle nos pontos de await. Isso reduz overhead e facilita lidar com muitas operações de I/O simultâneas.
Uma consequência prática: se você executar uma operação pesada de CPU dentro de uma coroutine sem ceder, você “trava” o loop e prejudica todas as outras tarefas.
async/await na prática: concorrência para I/O-bound
Considere um cenário comum: consultar várias URLs. Uma abordagem sequencial espera cada resposta antes de começar a próxima. Em uma abordagem concorrente com asyncio, você dispara várias requisições e aguarda todas.
Exemplo conceitual (sem biblioteca HTTP específica):
import asyncio
async def buscar(id_, delay):
await asyncio.sleep(delay)
return f"resultado-{id_}"
async def main():
tarefas = [buscar(1, 2), buscar(2, 1), buscar(3, 3)]
resultados = await asyncio.gather(*tarefas)
print(resultados)
asyncio.run(main())
O que acontece:
- As três operações “andam” juntas.
- O tempo total tende a se aproximar do maior delay, não da soma de todos.
Na vida real, para HTTP assíncrono, é comum usar bibliotecas compatíveis com asyncio (por exemplo, clientes que oferecem await em operações de rede). A regra é simples: não basta usar async, as bibliotecas chamadas também precisam ser assíncronas para evitar bloqueio.
asyncio.gather, create_task e o controle do agendamento
Dois padrões aparecem com frequência:
1) asyncio.gather()
Bom quando você quer disparar várias coroutines e esperar todas (ou tratar exceções de forma agregada).
resultados = await asyncio.gather(c1(), c2(), c3())
2) asyncio.create_task()
Útil quando você quer iniciar uma tarefa e deixá-la rodando “em paralelo” com outras operações dentro do mesmo loop.
t = asyncio.create_task(coroutine())
# ... faz outras coisas
await t
O cuidado aqui é garantir que tarefas criadas sejam aguardadas ou canceladas apropriadamente, evitando “tarefas órfãs” e comportamentos inesperados no encerramento.
Erros comuns em Concorrência em Python com asyncio
1) Bloquear o event loop com CPU-bound
Exemplo de problema: compressão, criptografia pesada, parsing massivo, machine learning, processamento de imagem em grande volume — tudo isso pode consumir CPU por tempo prolongado.
Solução típica:
- mover para processos (
multiprocessing,ProcessPoolExecutor) quando for CPU-bound; - ou delegar para thread/executor se for uma biblioteca que bloqueia mas você não tem alternativa assíncrona.
2) Chamar bibliotecas síncronas dentro de async def
Se dentro de uma coroutine você chama uma função que faz I/O bloqueante (por exemplo, um cliente HTTP síncrono), o loop para.
Mitigações:
- preferir bibliotecas assíncronas;
- quando não houver alternativa, usar
asyncio.to_thread()(ou executores) para isolar o bloqueio, sabendo que isso não transforma I/O síncrono em assíncrono “real”, apenas evita travar o loop.
3) Falhas de timeout e cancelamento
Em sistemas reais, você precisa de:
- timeouts por operação,
- cancelamento de tarefas em cascata,
- limpeza correta de recursos (sockets, conexões).
No asyncio, cancelamento é parte do fluxo normal. Uma tarefa pode receber CancelledError. Projetar o código para tolerar cancelamento é uma prática importante.
Coroutines, threads e processos: quando usar cada um
Um guia prático (sem promessas absolutas, porque depende do perfil do sistema):
- Asyncio (
async/await): melhor para alta concorrência de I/O (muitas conexões, muitas requisições, websockets, bots, crawlers bem comportados, servidores). - Threads: úteis para integrar com bibliotecas bloqueantes, chamadas de sistema ou I/O que não tem alternativa assíncrona. Em Python, threads não costumam escalar bem para CPU-bound devido ao GIL, mas podem ajudar em I/O bloqueante.
- Processos: melhor para CPU-bound e para aproveitar múltiplos núcleos.
Em arquitetura de produção, é comum combinar:
asynciopara orquestração e rede,- processos para tarefas pesadas,
- filas (internas ou externas) para desacoplar cargas.
Segurança e confiabilidade em código concorrente
Concorrência em Python também tem impacto direto em segurança operacional:
- Exaustão de recursos: abrir conexões ilimitadas ou criar tarefas sem limites pode causar negação de serviço involuntária (auto-DoS). Use semáforos (
asyncio.Semaphore) e limites de concorrência. - Timeouts e backoff: dependências externas falham. Sem timeout, o sistema acumula tarefas pendentes e degrada.
- Validação de entrada e SSRF: crawlers e serviços que “buscam URLs” devem validar destinos, resolver DNS com cuidado, restringir redes internas e impor allowlists, para evitar SSRF.
- Logging e rastreabilidade: com muitas tarefas simultâneas, logs sem correlação dificultam auditoria. Use IDs de correlação por request/tarefa.
A natureza assíncrona facilita escala, mas também amplifica efeitos de erros: um loop travado ou uma fila sem controle impacta todo o serviço.
Um passo a passo para começar (sem armadilhas)
- Identifique o tipo de carga: I/O-bound ou CPU-bound.
- Se for I/O-bound, escolha bibliotecas assíncronas (cliente HTTP, driver de banco, etc.).
- Estruture um
main()assíncrono e inicialize comasyncio.run(main()). - Use
gatherpara esperar grupos de tarefas ecreate_taskpara concorrência de longa duração. - Imponha limites de concorrência (semáforos), timeouts e retries com política clara.
- Monitore: latência, número de tarefas ativas, tempo de fila, erro por dependência externa.
- Para CPU-bound, descarte a ideia de “resolver com async” e use processos/executores.
Conclusão
O trio coroutines + async/await + event loop tornou o modelo assíncrono uma abordagem central para Concorrência em Python, principalmente quando o objetivo é lidar com muitas operações de entrada/saída com eficiência e previsibilidade. O ganho vem de evitar bloqueios, permitir que tarefas cooperem e aproveitar o tempo ocioso de espera por rede e disco.
Ao mesmo tempo, o modelo exige disciplina: bibliotecas compatíveis, limites de concorrência, tratamento de timeout/cancelamento e separação clara entre I/O-bound e CPU-bound. Com esses cuidados, asyncio se torna uma base sólida para serviços modernos, integrações e automações em escala.