O Modelo Mental do Event Loop
A concorrência do Asyncio confunde pessoas que já conhecem threads, porque parece paralelismo por fora, mas funciona com um princípio completamente diferente por baixo.
Todas as outras páginas nesta seção - corrotinas, tasks, gather, concorrência estruturada, ponte síncrona/assíncrona - são na verdade um único mecanismo visto de um ângulo diferente: um único scheduler executando uma peça de código Python por vez, alternando para outra peça exatamente quando a atual diz que não tem nada útil a fazer no momento.
Entender esse scheduler, não memorizar a superfície da API, é o que faz com que asyncio.gather versus asyncio.TaskGroup versus asyncio.to_thread pareçam uma ideia coerente em vez de três ferramentas não relacionadas para consultar.
Resumo
- Asyncio alcança concorrência em uma única thread do sistema operacional executando exatamente o código de uma corrotina por vez e alternando para outra sempre que a atual está esperando por algo externo, não executando código em paralelo.
- Por Que Importa: Interpretar mal este modelo leva a dois erros opostos: esperar paralelismo de CPU que asyncio nunca fornece, ou escrever uma função com aparência síncrona que bloqueia silenciosamente todas as outras corrotinas que compartilham o loop.
- Conceitos Chave: event loop, corrotina, awaitable, Task, agendamento cooperativo, concorrência estruturada.
- Quando Usar: Trabalho limitado por I/O com muitas esperas concorrentes - chamadas de rede, consultas a banco de dados, operações de arquivo - onde o custo é principalmente espera, não computação.
- Limitações / Trade-offs: Asyncio não paraleliza trabalho limitado por CPU, requer que todas as bibliotecas na cadeia de chamadas cooperem (uma única chamada bloqueante paralisa todo o loop) e torna o fluxo de controle mais difícil de raciocinar do que código síncrono linear.
- Tópicos Relacionados: threading, multiprocessing, concorrência estruturada, Global Interpreter Lock (GIL).
Fundamentos
Uma corrotina é o que você obtém ao chamar uma função async def - não o resultado de executá-la, mas um objeto de função pausado em sua primeira linha, esperando para ser impulsionado para frente.
Nada dentro de uma corrotina é realmente executado até que algo a aguarde ou a agende como uma Task.
O event loop é a peça que faz o direcionamento: ele mantém uma fila de corrotinas e callbacks que estão prontas para serem executadas, executa uma até que atinja um await em algo que ainda não terminou, e então passa para o próximo item pronto na fila.
await é o ponto exato de entrega - significa "não tenho nada útil a fazer até que isso termine, então deixe outra coisa rodar enquanto isso", que é todo o mecanismo em que o agendamento cooperativo se baseia.
Uma analogia útil é um único chef executando vários potes em um único fogão: o chef mexe o pote A, então enquanto o pote A ferve sem supervisão, o chef passa para o pote B, depois para o pote C, sempre fazendo exatamente uma coisa em qualquer instante, mas nunca ficando parado esperando um único pote terminar.
Isso é fundamentalmente diferente de contratar três chefs (threads) que podem genuinamente mexer três potes ao mesmo tempo, ou alugar três cozinhas (processos) que nem sequer compartilham um fogão.
Asyncio é o modelo de um chef: eficiente porque o chef nunca fica ocioso enquanto outra coisa poderia progredir, mas ainda limitado pelo fato de haver exatamente um chef fazendo exatamente uma coisa por vez.
Mecânicas e Interações
A fila de prontos do loop é o mecanismo que transforma "muitas esperas pendentes" em "alto throughput em uma thread": uma corrotina aguardando uma leitura de socket não ocupa o loop enquanto a rede está silenciosa, ela registra interesse e se afasta, e o loop gasta esse tempo executando outras corrotinas cujos dados já chegaram.
Uma Task é o que envolve uma corrotina para que o loop possa rastreá-la e retomá-la independentemente - asyncio.create_task agenda uma corrotina para ser executada no loop sem que o chamador a aguarde imediatamente, que é o que torna possível a execução concorrente de múltiplas corrotinas em primeiro lugar.
É aqui também que as duas maneiras de executar várias corrotinas juntas divergem genuinamente, e é uma fonte comum de surpresa: asyncio.gather não cancela os outros awaitables quando um deles levanta uma exceção - por padrão, ele propaga imediatamente essa primeira exceção enquanto os outros continuam rodando em segundo plano, sem supervisão, a menos que o chamador faça algo a respeito.
asyncio.TaskGroup (3.11+) existe especificamente para fechar essa lacuna: ele vincula a vida útil de cada task filho ao bloco async with envolvente, e se um filho levanta uma exceção, o grupo cancela ativamente seus irmãos antes que o bloco saia - uma diferença comportamental real, não apenas uma sintaxe mais agradável sobre a mesma garantia.
"Bloquear o loop" é a falha de modelo mental mais consequente no código asyncio: qualquer chamada síncrona que não cede - um loop de CPU intenso, uma leitura de arquivo bloqueante, um driver de DB síncrono - ocupa o único chef inteiramente, então todas as outras corrotinas agendadas nesse loop param pelo tempo exato que essa chamada leva, não apenas a corrotina que a fez.
import asyncio
async def blocks_everyone() -> None:
total = 0
for i in range(50_000_000): # CPU pura, nunca cede
total += i
# todas as outras corrotinas neste loop esperaram o tempo todo
async def cooperates() -> None:
await asyncio.to_thread(lambda: sum(range(50_000_000)))
# o trabalho de CPU foi executado em uma thread worker; o loop continuou servindo outrasA ponte síncrona/assíncrona - asyncio.to_thread, loop.run_in_executor, asyncio.run - é o mecanismo para cruzar entre "código que o loop está dirigindo" e "código que não pode cooperar", em ambas as direções: descarregar uma chamada bloqueante para um pool de threads para que o loop permaneça livre, ou entrar no loop a partir de código síncrono comum via asyncio.run.
Considerações Avançadas e Aplicações
Uma sutileza que confunde engenheiros experientes que migram de threads para asyncio: existe exatamente um event loop por thread, asyncio.run cria um, executa-o até a conclusão e o fecha, e você não pode aninhar uma segunda chamada asyncio.run dentro de um loop já em execução - servidores, em vez disso, mantêm um loop de longa duração para a vida útil do processo em vez de criar um por requisição.
A justiça do agendamento do loop também depende inteiramente de cada corrotina ceder em intervalos razoáveis; uma base de código async bem comportada é realmente uma base de código onde cada operação adjacente a I/O é nativamente async ou explicitamente conectada a uma thread, porque o loop não pode preempir uma corrotina que se recusa a aguardar.
Implementações alternativas de event loop existem para o mesmo contrato de agendamento com diferentes características de desempenho - uvloop, por exemplo, substitui o loop padrão de Python puro por um baseado em libuv para I/O de menor latência, sem alterar nada sobre o modelo de corrotina/Task/await descrito aqui.
O Global Interpreter Lock (GIL) ainda importa dentro de um único loop asyncio: mesmo na build experimental free-threaded do CPython introduzida em 3.13 e continuada em 3.14, o contrato do asyncio de "o bytecode de uma corrotina é executado por vez neste loop" não mudou - o free-threading afeta o paralelismo verdadeiro multi-core de threads de SO separadas, não o modelo de agendamento cooperativo de loop único em si.
A observabilidade importa mais em asyncio do que em código síncrono precisamente porque falhas são falhas de agendamento, não apenas falhas de lógica: o modo de depuração do asyncio (PYTHONASYNCIODEBUG=1, ou asyncio.run(main(), debug=True)) expõe callbacks lentos e corrotinas nunca aguardadas que, de outra forma, falhariam silenciosamente.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
Corrotinas async/await nativas | Baratas de agendar, milhares de esperas concorrentes em uma thread | Inúteis para trabalho limitado por CPU; cada dependência deve cooperar | I/O de alto fan-out - APIs, chamadas de DB, buscas de rede |
threading | Concorrência real em nível de SO para chamadas bloqueantes; modelo mental simples por chamada | GIL ainda serializa bytecode Python; custa mais memória por worker | Conectar uma ou duas bibliotecas bloqueantes em um aplicativo async |
multiprocessing | Execução de CPU paralela real em vários núcleos | Custo pesado de inicialização de processo, sem memória compartilhada, sobrecarga de pickling | Trabalho limitado por CPU - parsing, compressão, lotes numéricos |
asyncio.to_thread / run_in_executor | Mantém o loop responsivo enquanto uma chamada bloqueante é executada em outro lugar | Adiciona um salto de thread pool; ainda não é paralelismo de CPU real | Chamadas bloqueantes isoladas dentro de uma base de código predominantemente async |
Concepções Errôneas Comuns
- "asyncio executa meu código em paralelo." Ele executa o código de uma corrotina por vez em uma thread; a concorrência vem da troca em pontos de
awaitenquanto outra coisa espera, não da execução simultânea. - "gather cancela as outras tasks quando uma falha." Por padrão, ele não cancela - os awaitables restantes continuam rodando a menos que você os trate explicitamente;
TaskGroupé a ferramenta que realmente cancela os irmãos em caso de falha. - "Criar uma Task e seguir em frente é bom - ela terminará sozinha." Uma task órfã sem referência e sem nada aguardando-a pode ser coletada silenciosamente pelo garbage collector em meio à execução ou simplesmente nunca ter suas exceções verificadas, que é exatamente o vazamento que a concorrência estruturada (
TaskGroup) fecha. - "await asyncio.sleep(0) não faz nada de verdade." É um ponto de cessão genuíno - ele devolve o controle ao loop por uma iteração, mesmo que nenhum tempo real passe, o que às vezes é usado deliberadamente para permitir que outras corrotinas progridam.
- "Tornar uma função async automaticamente a torna mais rápida." Isso só ajuda quando a função gasta tempo real esperando por I/O; envolver trabalho limitado por CPU em
async defsem descarregá-lo para uma thread ou processo não muda nada sobre quanto tempo leva e pode piorar todo o loop ao bloqueá-lo.
FAQs
O que exatamente é o event loop?
O scheduler que mantém uma fila de corrotinas e callbacks prontos para serem executados, executa um por vez e alterna para o próximo sempre que o atual aguarda algo que ainda não está pronto.
Asyncio é realmente paralelo?
Não. É concorrente em uma única thread - o código Python de uma corrotina é executado em qualquer instante, e o progresso em outras ocorre intercalando em pontos de await, não executando simultaneamente.
Qual é a diferença entre uma corrotina, uma Task e um Future?
Uma corrotina é um objeto de função pausado que não faz nada até ser aguardado ou agendado. Uma Task envolve uma corrotina para que o loop possa executá-la e rastreá-la independentemente do chamador. Um Future é a primitiva de nível inferior que representa um resultado eventual no qual as Tasks são construídas.
Como await realmente devolve o controle para o loop?
Quando uma corrotina atinge um await em algo que ainda não está pronto, ela suspende naquele ponto e retorna o controle para o loop, que então executa qualquer outra corrotina pronta ou callback que esteja em seguida em sua fila.
Por que só posso ter um event loop por thread?
O loop possui o agendamento da thread enquanto estiver em execução; um segundo loop na mesma thread não teria como se intercalar com o primeiro, então asyncio impõe um loop ativo por thread e asyncio.run não aninhará dentro de um loop já em execução.
Qual é a diferença real entre gather e TaskGroup?
gather executa awaitables concorrentemente e, por padrão, permite que os outros continuem rodando em segundo plano se um levantar uma exceção. TaskGroup vincula a vida útil de cada filho ao seu bloco async with e cancela ativamente os filhos restantes quando um falha, fornecendo garantias de concorrência estruturada mais fortes.
O que significa "bloquear o event loop" na prática?
Significa executar uma operação síncrona dentro de uma corrotina que nunca cede - um loop limitado por CPU, uma chamada de arquivo ou DB bloqueante - que ocupa o loop inteiramente e paralisa todas as outras corrotinas agendadas nele pela duração total dessa chamada.
Quando devo usar asyncio.to_thread em vez de simplesmente tornar algo async?
Quando a operação é fornecida por uma biblioteca que não tem versão async - um driver de DB bloqueante, uma computação limitada por CPU, uma API síncrona legada - to_thread a move para fora da thread do loop para que o loop continue servindo outras corrotinas enquanto ela é executada.
O GIL ainda importa em código asyncio?
Sim - dentro de um único loop, o bytecode de uma corrotina é executado por vez, independentemente do status do GIL, porque o modelo cooperativo do asyncio já serializa a execução; mesmo na build free-threaded do CPython (experimental desde 3.13, continuando em 3.14), esse contrato de agendamento para um único loop não muda.
O que é uvloop e por que eu o usaria?
É uma implementação de event loop substituta direta (drop-in) baseada em libuv, oferecendo menor latência e maior throughput para cargas de trabalho com I/O intensivo do que o loop padrão de Python puro, sem alterar o modelo de programação de corrotina/Task/await.
Como depuro problemas de agendamento em vez de bugs de lógica?
Ative o modo de depuração do asyncio (PYTHONASYNCIODEBUG=1 ou asyncio.run(main(), debug=True)), que expõe callbacks lentos, corrotinas nunca aguardadas e outros problemas em nível de agendamento que um traceback normal não mostraria.
Devo reescrever toda a minha base de código para asyncio para obter desempenho?
Somente se o gargalo for tempo gasto esperando por I/O com alta concorrência; trabalho limitado por CPU não ganha nada apenas com asyncio e precisa de multiprocessing ou descarregamento de thread/processo, independentemente de o código circundante ser async.
Por que uma única chamada bloqueante não aguardada em qualquer lugar da cadeia de chamadas importa tanto?
Porque o loop não pode preempir uma corrotina no meio da execução da maneira que um scheduler de SO preempir uma thread - ele só recupera o controle em um await, então uma função que nunca cede bloqueia o loop inteiro pela duração total dela, não apenas sua unidade de trabalho lógica.
Relacionados
- Noções Básicas de Asyncio - exemplos práticos de corrotina, gather e timeout
- Tasks & Gather - a API Task na qual este modelo de agendamento é construído
- Concorrência Estruturada - as garantias de cancelamento do
TaskGroupem profundidade - Ponte Síncrona/Assíncrona -
to_threaderun_in_executorna prática - Depurando Código Async - diagnosticando travamentos e tasks órfãs
Versões da Stack: Esta página foi escrita para Python 3.14.0 (3.14 estável, 3.13 em manutenção), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+ e uv 0.6+.