M17 — Async/await
Bloco 8 — Reuso e integração · Eixo B · Nível NÍVEL 1 + NÍVEL 2 · LAB
Abertura
Até agora, todo código do curso roda de forma sequencial: uma linha termina antes da próxima começar. Isso é simples, mas ineficiente quando o programa precisa esperar por algo externo — como uma resposta de rede. Este é o último módulo do Eixo B antes do Agent SDK (M18), e o conceito que ele introduz é essencial para entender como um agente pode fazer várias coisas "ao mesmo tempo" sem travar.
Conceito central
Código síncrono executa uma instrução de cada vez, esperando cada uma terminar. Quando uma operação envolve espera — uma requisição de rede, a leitura de um arquivo grande — o programa fica parado sem fazer nada útil enquanto espera.
Código assíncrono permite que o programa inicie uma operação de espera
e, enquanto ela não termina, siga fazendo outras coisas — é uma
operação não bloqueante.
Em Python, isso é expresso com as palavras-chave
async e
await: uma função declarada com
async def é uma corrotina;
dentro dela, await marca o ponto onde o programa pode "ceder a vez"
enquanto espera algo terminar.
É importante deixar claro: assíncrono não significa paralelo (rodando ao mesmo tempo em núcleos diferentes de processador) — significa não bloquear enquanto espera, o que é diferente e mais sutil. Conectando com o Eixo A: quando o Claude Code executa múltiplas ferramentas ou subagents em paralelo (M13), o mecanismo por trás é conceitualmente aparentado a isso — não ficar parado esperando uma coisa terminar para começar a próxima.
Na prática
O sandbox desta página roda um interpretador Python simplificado (Pyodide) dentro do navegador. Ele suporta
async/awaiteasyncio.sleep/asyncio.gather, mas não suportaasyncio.run()do mesmo jeito que o Python instalado no seu computador — porque o próprio sandbox já roda dentro do equivalente a um loop assíncrono. Por isso, os exemplos abaixo usamawaitdiretamente, semasyncio.run(). No seu computador, ou no Claude Code, os dois estilos funcionam. Vale notar também que este sandbox roda numa única thread do navegador — então os exemplos abaixo mostram bem a diferença entre não bloquear (o queasynciofaz de verdade) e rodar em núcleos de processador diferentes ao mesmo tempo (o queasyncionão faz): aqui não existe a menor chance de duas tarefas rodarem literalmente ao mesmo tempo, só de alternar entre elas de forma eficiente.
Duas esperas rodando uma depois da outra — o tempo total é a soma das duas:
As mesmas duas esperas, agora rodando ao mesmo tempo com
asyncio.gather() — o tempo total é o da mais demorada, não a soma:
Repare no resultado: a primeira versão leva cerca de 2 segundos (1 + 1, em sequência); a segunda leva cerca de 1 segundo (as duas esperas acontecem ao mesmo tempo). O código de cada consulta não mudou — o que mudou foi como elas foram organizadas para rodar.
O exemplo anterior mostra o ganho de tempo, mas não prova que as duas tarefas rodam "ao mesmo tempo" de verdade. Este aqui mostra a concorrência por dentro, imprimindo o início e o fim de cada tarefa:
Repare na ordem dos prints: as duas tarefas "começam" quase juntas, mas
"terminam" na ordem do próprio tempo de espera de cada uma — Tarefa B
(1s) antes de Tarefa A (2s). Isso só é possível porque existe uma única
linha de execução alternando entre as duas nos pontos marcados por
await: nunca duas rodando ao mesmo tempo em núcleos diferentes do
processador (isso seria paralelismo, M13), e sim uma cedendo a vez para
a outra enquanto espera — concorrência.
Diagrama
Para onde vai o tempo de espera, em cada versão:
Lab opcional 🔵
Escreva duas corrotinas que simulam chamadas demoradas (com
asyncio.sleep) e execute-as concorrentemente com asyncio.gather(),
comparando o tempo total com a versão sequencial.