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.
Busque em todas as páginas da documentação
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.
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.
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.
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 |
await enquanto outra coisa espera, não da execução simultânea.TaskGroup é a ferramenta que realmente cancela os irmãos em caso de falha.TaskGroup) fecha.async def sem descarregá-lo para uma thread ou processo não muda nada sobre quanto tempo leva e pode piorar todo o loop ao bloqueá-lo.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.
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.
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.
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.
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.
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.
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 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.
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.
É 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.
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.
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.
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.
TaskGroup em profundidadeto_thread e run_in_executor na práticaVersõ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+.
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026