Python Programação Assíncrona (asyncio)

Muitos programas passam a maior parte do tempo esperando: a resposta de um site, um arquivo que vem pela rede, um banco de dados que demora a responder. Enquanto espera, um programa comum fica parado, sem fazer mais nada. A programação assíncrona ensina o programa a aproveitar essas esperas: começar outra tarefa enquanto a primeira não responde. Em Python, ela se faz com duas palavras novas, async e await, e com o módulo asyncio. É o assunto mais avançado deste nível; vá devagar, rodando cada exemplo.

Você vai aprender
  • A ideia: uma pessoa só, alternando tarefas nas esperas.
  • A escrever uma corrotina com async def e a esperar com await.
  • A rodar tudo com asyncio.run().
  • A fazer várias esperas ao mesmo tempo com asyncio.gather(), e por que o tempo total cai.
  • create_task, TaskGroup e wait_for (tempo limite).
  • O que acontece quando uma das tarefas dá erro.
  • Os erros e avisos reais: coroutine was never awaited, 'await' outside function, a coroutine or an awaitable is required e o time.sleep que trava tudo.

A ideia: a cozinheira que não fica olhando o forno

A Ana prepara o café da manhã: o pão leva 2 minutos no forno, e o café leva 1 minuto para a água ferver. Há dois jeitos de fazer:

  • Um de cada vez (síncrono): põe o pão no forno e fica parada olhando até ficar pronto (2 min). Só então põe a água do café e espera ferver (1 min). Total: 3 minutos.
  • Aproveitando a espera (assíncrono): põe o pão no forno e, enquanto ele assa, põe a água do café. O café fica pronto no primeiro minuto, o pão no segundo. Total: 2 minutos.

Repare: continua sendo uma pessoa só. Ela não fica mais rápida nem ganha um par de braços; ela só não desperdiça a espera. É isso que o asyncio faz: um programa, uma linha de execução, alternando entre tarefas toda vez que uma delas diz "agora vou esperar".

Na cozinhaNo Python
Uma receita que tem esperas no meioUma corrotina: async def assar_pao():
"Agora vou esperar; pode cuidar de outra coisa"await (em inglês, "aguardar")
Esperar o fornoawait asyncio.sleep(2)
A cozinheira, que decide o que fazer a cada momentoO laço de eventos (event loop) do asyncio
Abrir a cozinha e começar o diaasyncio.run(main())
"Faça o pão e o café juntos"await asyncio.gather(assar_pao(), fazer_cafe())

1. O jeito de sempre: um de cada vez

Para comparar, o café da manhã com funções comuns. O time.sleep(segundos) faz o programa dormir (usamos segundos no lugar dos minutos, para não esperar tanto), e o time.perf_counter() mede o tempo, como no @cronometro da aula Decoradores:

import time


def fazer_cafe():
    print("Café: pus a água para ferver")
    time.sleep(1)
    print("Café: pronto")


def assar_pao():
    print("Pão: pus no forno")
    time.sleep(2)
    print("Pão: pronto")


inicio = time.perf_counter()
assar_pao()
fazer_cafe()
print(f"Café da manhã em {time.perf_counter() - inicio:.1f} s")
$ python3 sincrono.py
Pão: pus no forno
Pão: pronto
Café: pus a água para ferver
Café: pronto
Café da manhã em 3.0 s

2 + 1 = 3 segundos. Durante os 2 segundos do forno, o programa não fez absolutamente nada.


2. A primeira corrotina: async def, await e asyncio.run

import asyncio


async def dizer_ola():
    print("Olá...")
    await asyncio.sleep(1)
    print("...do mundo assíncrono!")


asyncio.run(dizer_ola())
$ python3 primeira.py
Olá...
...do mundo assíncrono!
import asyncio
O módulo da programação assíncrona. Vem com o Python.
async def dizer_ola():
async na frente do def cria uma corrotina: uma função que pode pausar no meio para esperar, e deixar outras corrotinas trabalharem enquanto isso. (Lembra o yield da aula Geradores, que também pausa; as corrotinas nasceram dos geradores.)
await asyncio.sleep(1)
"Aguarde 1 segundo". É a versão assíncrona do time.sleep: em vez de travar o programa, ela avisa o laço de eventos "vou esperar 1 segundo; se tiver outra tarefa, cuide dela". O await só pode aparecer dentro de um async def.
asyncio.run(dizer_ola())
Liga o laço de eventos, roda a corrotina até o fim e desliga. É a porta de entrada: normalmente aparece uma vez, na última linha do programa.

Chamar uma corrotina não a executa

Como nos geradores, chamar dizer_ola() não roda nenhuma linha: só cria um objeto corrotina, que precisa ser entregue ao asyncio:

import asyncio


async def dizer_ola():
    print("Olá!")


resultado = dizer_ola()
print(resultado)
print(type(resultado))
asyncio.run(resultado)
$ python3 corrotina.py
<coroutine object dizer_ola at 0x7f2eac4c1c00>      « o endereço muda a cada execução
<class 'coroutine'>
Olá!

O Olá! só apareceu na última linha, quando o asyncio.run executou a corrotina. Esquecer o asyncio.run (ou o await) é o erro mais comum; ele está em Quando dá errado.


3. Só await não basta: ainda é um de cada vez

O café da manhã com corrotinas, cada uma com o seu await asyncio.sleep. A main ("principal") é a corrotina que organiza as outras, um costume do asyncio:

import asyncio
import time


async def fazer_cafe():
    print("Café: pus a água para ferver")
    await asyncio.sleep(1)
    print("Café: pronto")
    return "café"


async def assar_pao():
    print("Pão: pus no forno")
    await asyncio.sleep(2)
    print("Pão: pronto")
    return "pão"


async def main():
    inicio = time.perf_counter()
    await assar_pao()
    await fazer_cafe()
    print(f"Café da manhã em {time.perf_counter() - inicio:.1f} s")


asyncio.run(main())
$ python3 sequencial.py
Pão: pus no forno
Pão: pronto
Café: pus a água para ferver
Café: pronto
Café da manhã em 3.0 s

Os mesmos 3 segundos! await assar_pao() quer dizer "espere o pão terminar, e só então siga para a próxima linha". A main esperou o pão inteiro antes de começar o café. Para fazer as duas ao mesmo tempo, é preciso dizer isso ao asyncio.


4. asyncio.gather: várias ao mesmo tempo

asyncio.gather(…) ("reunir") recebe várias corrotinas, começa todas e espera todas terminarem. Só a main muda:

import asyncio
import time


async def fazer_cafe():
    print("Café: pus a água para ferver")
    await asyncio.sleep(1)
    print("Café: pronto")
    return "café"


async def assar_pao():
    print("Pão: pus no forno")
    await asyncio.sleep(2)
    print("Pão: pronto")
    return "pão"


async def main():
    inicio = time.perf_counter()
    resultados = await asyncio.gather(assar_pao(), fazer_cafe())
    print("Na mesa:", resultados)
    print(f"Café da manhã em {time.perf_counter() - inicio:.1f} s")


asyncio.run(main())
$ python3 gather.py
Pão: pus no forno
Café: pus a água para ferver
Café: pronto
Pão: pronto
Na mesa: ['pão', 'café']
Café da manhã em 2.0 s
  1. O gather começa o pão: Pão: pus no forno. No await asyncio.sleep(2), o pão diz "vou esperar".
  2. O laço de eventos aproveita e começa o café: Café: pus a água para ferver. No await asyncio.sleep(1), o café também espera.
  3. Com 1 segundo, a espera do café acaba primeiro: Café: pronto.
  4. Com 2 segundos, a do pão: Pão: pronto.
  5. O gather devolve a lista com o que cada corrotina devolveu (o return), na ordem em que foram passadas (pão, café), e não na ordem em que terminaram.

Tempo total: 2 segundos, o tempo da espera mais longa, e não a soma. (Os números do tempo podem variar um pouco, 2.0 ou 2.1, conforme o computador.)

Missão: a main espera o pão terminar antes de começar o café, e por isso o café fica pronto depois do pão. Troque as duas linhas de await por uma só: await asyncio.gather(assar_pao(), fazer_cafe()). O Café: pronto tem de aparecer antes do Pão: pronto.

import asyncio


async def fazer_cafe():
    print("Café: pus a água para ferver")
    await asyncio.sleep(1)
    print("Café: pronto")


async def assar_pao():
    print("Pão: pus no forno")
    await asyncio.sleep(2)
    print("Pão: pronto")


async def main():
    await assar_pao()
    await fazer_cafe()


asyncio.run(main())
import asyncio


async def fazer_cafe():
    print("Café: pus a água para ferver")
    await asyncio.sleep(1)
    print("Café: pronto")


async def assar_pao():
    print("Pão: pus no forno")
    await asyncio.sleep(2)
    print("Pão: pronto")


async def main():
    await asyncio.gather(assar_pao(), fazer_cafe())


asyncio.run(main())

Cem esperas, quase o tempo de uma

A vantagem cresce com o número de tarefas. Cem entregas, cada uma esperando 0,1, 0,2 ou 0,3 segundo (o *tarefas desempacota a lista em argumentos, como na aula *args e **kwargs):

import asyncio
import time


async def entregar(pedido, minutos):
    await asyncio.sleep(minutos / 10)
    return f"pedido {pedido} entregue"


async def main():
    inicio = time.perf_counter()
    tarefas = [entregar(n, n % 3 + 1) for n in range(1, 101)]
    resultados = await asyncio.gather(*tarefas)
    print(len(resultados), "entregas")
    print(resultados[:3])
    print(f"Tempo total: {time.perf_counter() - inicio:.1f} s")


asyncio.run(main())
$ python3 muitos.py
100 entregas
['pedido 1 entregue', 'pedido 2 entregue', 'pedido 3 entregue']
Tempo total: 0.3 s

Somadas, as esperas dariam uns 20 segundos. Juntas, levaram 0,3 s: o tempo da mais longa. É assim que um programa assíncrono consegue, por exemplo, baixar cem páginas da internet quase no tempo de uma: o tempo todo é espera pela rede.


5. Tarefas: começar agora, esperar depois

O gather começa e espera tudo de uma vez. Às vezes você quer começar algo e só esperar lá na frente, fazendo outras coisas no meio. Para isso existe asyncio.create_task(corrotina) ("criar tarefa"): ele agenda a corrotina e devolve uma tarefa, que você aguarda quando quiser.

import asyncio


async def lavar_roupa():
    print("Máquina: começou")
    await asyncio.sleep(2)
    print("Máquina: terminou")
    return "roupa limpa"


async def main():
    maquina = asyncio.create_task(lavar_roupa())
    print("Enquanto a máquina bate, a Ana arruma a cozinha.")
    await asyncio.sleep(1)
    print("Cozinha arrumada.")
    roupa = await maquina
    print("Resultado:", roupa)


asyncio.run(main())
$ python3 tarefas.py
Enquanto a máquina bate, a Ana arruma a cozinha.
Máquina: começou
Cozinha arrumada.
Máquina: terminou
Resultado: roupa limpa

Repare na ordem: a frase da Ana veio antes de Máquina: começou. O create_task só agenda; a tarefa começa de verdade no primeiro await da main, quando a main para de trabalhar e deixa o laço de eventos cuidar das outras. Lá no fim, await maquina espera a máquina terminar e pega o return dela.

TaskGroup: um grupo de tarefas

Desde o Python 3.11, o jeito recomendado de começar várias tarefas e esperar todas é o asyncio.TaskGroup ("grupo de tarefas"), um gerenciador de contexto (aula Gerenciadores de Contexto) usado com async with:

import asyncio


async def preparar(item, segundos):
    await asyncio.sleep(segundos)
    print(f"{item} pronto")
    return item


async def main():
    async with asyncio.TaskGroup() as grupo:
        t1 = grupo.create_task(preparar("bolo", 2))
        t2 = grupo.create_task(preparar("suco", 1))
    print("Tudo pronto:", t1.result(), t2.result())


asyncio.run(main())
$ python3 taskgroup.py
suco pronto
bolo pronto
Tudo pronto: bolo suco

O bloco async with só termina quando todas as tarefas do grupo terminaram; depois dele, .result() dá o que cada uma devolveu. A diferença para o gather aparece quando algo dá errado: se uma tarefa do grupo falha, o TaskGroup cancela as outras, em vez de deixá-las rodando soltas.

wait_for: tempo limite

Esperar, sim; para sempre, não. asyncio.wait_for(corrotina, timeout=segundos) desiste depois do tempo dado e lança TimeoutError ("erro de tempo esgotado"), que você trata com try/except (aula try e except):

import asyncio


async def esperar_entregador():
    await asyncio.sleep(5)
    return "pizza"


async def main():
    try:
        pizza = await asyncio.wait_for(esperar_entregador(), timeout=1)
        print("Chegou:", pizza)
    except TimeoutError:
        print("Demorou demais: vou fazer um sanduíche.")


asyncio.run(main())
$ python3 timeout.py
Demorou demais: vou fazer um sanduíche.

O programa terminou em 1 segundo, e não em 5: o wait_for cancelou a espera do entregador.


6. Quando uma das tarefas dá erro

import asyncio


async def assar_pao():
    await asyncio.sleep(1)
    return "pão"


async def fazer_cafe():
    await asyncio.sleep(0.5)
    raise ValueError("acabou o pó de café")


async def main():
    resultados = await asyncio.gather(assar_pao(), fazer_cafe(), return_exceptions=True)
    print(resultados)
    resultados = await asyncio.gather(assar_pao(), fazer_cafe())
    print(resultados)


asyncio.run(main())
$ python3 erro_gather.py
['pão', ValueError('acabou o pó de café')]
Traceback (most recent call last):
  File "/home/ana/erro_gather.py", line 21, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "/usr/lib/python3.14/asyncio/runners.py", line 204, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
«… mais 6 linhas de dentro do asyncio …»
  File "/home/ana/erro_gather.py", line 17, in main
    resultados = await asyncio.gather(assar_pao(), fazer_cafe())
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ana/erro_gather.py", line 11, in fazer_cafe
    raise ValueError("acabou o pó de café")
ValueError: acabou o pó de café
  • Com return_exceptions=True ("devolver as exceções"), o erro do café não para nada: ele vira um item da lista de resultados, no lugar do café. O pão chegou normalmente. Depois, você confere cada item.
  • Sem essa opção, o primeiro erro sobe pelo await do gather e, como ninguém trata, encerra o programa.
  • O traceback passa por várias linhas de dentro do asyncio (o asyncio.run, o laço de eventos). Para entender o erro, olhe as linhas que são do seu arquivo, de baixo para cima: a linha 11, na fazer_cafe, lançou o erro; a linha 17, na main, estava esperando no gather. No editor do guia, o traceback sai igual a este.

Formas diferentes de esperar várias coisas

FormaQuando usar
await a() e depois await b()Quando b precisa do resultado de a (não dá para começar o recheio antes de assar a massa)
await asyncio.gather(a(), b())Esperas independentes, e você quer a lista dos resultados na ordem
asyncio.create_task(a()) … await tarefaComeçar agora e aguardar lá na frente, fazendo outra coisa no meio
async with asyncio.TaskGroup()Várias tarefas, e, se uma falhar, as outras devem ser canceladas (3.11+)
asyncio.wait_for(a(), timeout=n)Desistir depois de n segundos
Quando o asyncio ajuda (e quando não)

O asyncio acelera programas que passam o tempo esperando coisas de fora: rede, sites, bancos de dados, muitos arquivos. Ele não acelera contas pesadas (somar um bilhão de números, processar imagens): ali não há espera para aproveitar, a cozinheira está o tempo todo com as mãos ocupadas. E só funciona com funções que sabem "esperar sem travar": o asyncio.sleep sim, o time.sleep não (veja abaixo). Para a rede, existem bibliotecas assíncronas instaláveis com o pip; elas não funcionam no editor do guia, que não tem acesso à rede.


Quando dá errado

time.sleep dentro de uma corrotina: trava todo mundo
import asyncio
import time


async def fazer_cafe():
    print("Café: pus a água para ferver")
    time.sleep(1)
    print("Café: pronto")


async def assar_pao():
    print("Pão: pus no forno")
    time.sleep(2)
    print("Pão: pronto")


async def main():
    inicio = time.perf_counter()
    await asyncio.gather(assar_pao(), fazer_cafe())
    print(f"Café da manhã em {time.perf_counter() - inicio:.1f} s")


asyncio.run(main())
$ python3 bloqueia.py
Pão: pus no forno
Pão: pronto
Café: pus a água para ferver
Café: pronto
Café da manhã em 3.0 s

Tem async, tem gather, e mesmo assim levou 3 segundos, um de cada vez. Nenhum erro, nenhum aviso. O time.sleep não sabe nada de asyncio: ele trava o programa inteiro, e o laço de eventos não tem chance de começar o café. Só o await devolve a vez. Como consertar: dentro de async def, use await asyncio.sleep(…). A regra vale para tudo que demora: precisa ser uma versão assíncrona, chamada com await.

Esqueceu o await
import asyncio
import time


async def main():
    inicio = time.perf_counter()
    print("Esperando 1 segundo...")
    asyncio.sleep(1)
    print(f"Esperei {time.perf_counter() - inicio:.1f} s")


asyncio.run(main())
$ python3 sem_await.py
Esperando 1 segundo...
/home/ana/sem_await.py:8: RuntimeWarning: coroutine 'sleep' was never awaited
  asyncio.sleep(1)
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
Esperei 0.0 s

"Aviso de execução: a corrotina sleep nunca foi aguardada". Sem await, asyncio.sleep(1) só criou uma corrotina (como na seção 2) e a jogou fora: não esperou nada, como mostra o 0.0 s. O aviso diz o arquivo e a linha (8). A segunda linha do aviso, sobre o tracemalloc, é uma ferramenta de investigação de memória e pode ser ignorada. Como consertar: await asyncio.sleep(1).

Chamou a main() sem o asyncio.run
import asyncio


async def main():
    print("O café da manhã começou.")
    await asyncio.sleep(1)


main()
print("Fim do programa.")
$ python3 sem_run.py
/home/ana/sem_run.py:9: RuntimeWarning: coroutine 'main' was never awaited
  main()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
Fim do programa.

O mesmo aviso, agora para a main. O O café da manhã começou. nunca apareceu: a corrotina foi criada na linha 9 e nunca executada. Como consertar: asyncio.run(main()).

await fora de uma corrotina
import asyncio

print("Esperando...")
await asyncio.sleep(1)
$ python3 await_fora.py
  File "/home/ana/await_fora.py", line 4
    await asyncio.sleep(1)
    ^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: 'await' outside function

"await fora de função". É um erro de sintaxe: o Python recusa o arquivo antes de rodar qualquer linha (repare que nem o Esperando... apareceu, e não há a linha Traceback). O await só existe dentro de async def. Como consertar: ponha o código numa async def main(): e rode com asyncio.run(main()).

asyncio.run(main), sem os parênteses
$ python3 run_sem_parenteses.py
Traceback (most recent call last):
  File "/home/ana/run_sem_parenteses.py", line 8, in <module>
    asyncio.run(main)
    ~~~~~~~~~~~^^^^^^
  File "/usr/lib/python3.14/asyncio/runners.py", line 204, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/usr/lib/python3.14/asyncio/runners.py", line 103, in run
    raise TypeError('An asyncio.Future, a coroutine or an '
                    'awaitable is required')
TypeError: An asyncio.Future, a coroutine or an awaitable is required

"É preciso um Future do asyncio, uma corrotina ou algo aguardável". Sem parênteses, foi a função main, e não a corrotina que ela cria (a mesma diferença entre f e f() da aula Decoradores). Como consertar: asyncio.run(main()).


Referência rápida

EscritaO que faz
async def nome():Define uma corrotina; chamá-la cria um objeto coroutine, sem rodar
await algoEspera algo terminar, deixando outras tarefas rodarem; só dentro de async def
asyncio.run(main())Liga o laço de eventos e roda a corrotina principal até o fim
await asyncio.sleep(s)Espera s segundos sem travar as outras
await asyncio.gather(a(), b())Roda juntas; devolve a lista dos resultados na ordem dos argumentos
gather(…, return_exceptions=True)Os erros viram itens da lista, em vez de subir
t = asyncio.create_task(a()) / await tAgenda agora / espera e pega o resultado depois
async with asyncio.TaskGroup() as g:Grupo de tarefas (g.create_task); espera todas no fim do bloco (3.11+)
await asyncio.wait_for(a(), timeout=s)Desiste depois de s segundos com TimeoutError
SintomaCausa provável
RuntimeWarning: coroutine '…' was never awaitedFaltou o await, ou o asyncio.run
<coroutine object … at 0x…> no printMostrou a corrotina sem executá-la
SyntaxError: 'await' outside functionawait fora de um async def
TypeError: An asyncio.Future, a coroutine or an awaitable is requiredasyncio.run(main) sem os parênteses
O gather não ficou mais rápidotime.sleep (ou outra função que trava) dentro da corrotina
Ainda um de cada vezawait a() seguido de await b(): use gather ou tarefas
TimeoutErrorO wait_for esgotou o tempo: trate com try/except

Exercícios

1. A palavra da corrotina

Complete para criar uma corrotina:

def baixar_pagina():

2. Esperar sem travar

Complete a linha que espera 2 segundos dentro de uma corrotina, deixando as outras tarefas rodarem:

3. A porta de entrada

Complete a última linha do programa, que roda a corrotina main:

4. Três corrotinas esperam 1, 2 e 3 segundos com asyncio.sleep e rodam com gather. Quanto tempo leva, mais ou menos?

5. await asyncio.gather(pao(), cafe()), em que o café termina primeiro. Em que ordem vem a lista de resultados?

6. Em qual destes programas o asyncio ajuda mais?

Missão final: os pedidos da lanchonete

Missão: a lanchonete recebeu três pedidos: pizza (3 s), pastel (2 s) e suco (1 s). Rode e leia o aviso: o programa nem começou. Conserte duas coisas: (1) a última linha deve ser asyncio.run(main()); (2) na main, prepare os três ao mesmo tempo com pedidos = await asyncio.gather(…), na ordem pizza, pastel, suco, e mostre Pedidos prontos: com a lista. O suco deve ficar pronto primeiro.

import asyncio


async def preparar(item, segundos):
    print(f"Começando: {item}")
    await asyncio.sleep(segundos)
    print(f"Pronto: {item}")
    return item


async def main():
    pizza = await preparar("pizza", 3)
    pastel = await preparar("pastel", 2)
    suco = await preparar("suco", 1)
    print("Pedidos prontos:", [pizza, pastel, suco])


main()
import asyncio


async def preparar(item, segundos):
    print(f"Começando: {item}")
    await asyncio.sleep(segundos)
    print(f"Pronto: {item}")
    return item


async def main():
    pedidos = await asyncio.gather(
        preparar("pizza", 3),
        preparar("pastel", 2),
        preparar("suco", 1),
    )
    print("Pedidos prontos:", pedidos)


asyncio.run(main())

A solução mostra os três Começando: … de uma vez, depois Pronto: suco, Pronto: pastel e Pronto: pizza, nessa ordem, e por fim Pedidos prontos: ['pizza', 'pastel', 'suco']: a lista na ordem do gather, não na ordem em que ficaram prontos. Tudo em uns 3 segundos, em vez de 6. No terminal da Ana:

$ python3 lanche_sol.py
Começando: pizza
Começando: pastel
Começando: suco
Pronto: suco
Pronto: pastel
Pronto: pizza
Pedidos prontos: ['pizza', 'pastel', 'suco']

Resumo

  • Programação assíncrona é aproveitar as esperas: um programa só, alternando entre tarefas sempre que uma delas espera.
  • async def cria uma corrotina; chamá-la só cria o objeto. Ela roda com await (dentro de outra corrotina) ou com asyncio.run(main()) (na última linha do programa).
  • await a() seguido de await b() ainda é um de cada vez. Para juntos: asyncio.gather(a(), b()), que devolve os resultados na ordem dos argumentos; ou create_task e TaskGroup.
  • wait_for põe tempo limite (TimeoutError); return_exceptions=True transforma os erros do gather em itens da lista.
  • Dentro de corrotina, nada de time.sleep: ele trava tudo. Use await asyncio.sleep.
  • coroutine '…' was never awaited quer dizer: faltou um await ou o asyncio.run.