asyncio na prática
Esta aula explora o módulo asyncio do Python na prática, cobrindo criação de tarefas com create_task, agregação de corrotinas com gather, controle de tempo com timeouts e operações de I/O assíncrono. O conteúdo inclui exemplos de código, boas práticas e exercícios para consolidar o aprendizado.
Nesta aula, vamos mergulhar no módulo asyncio do Python, essencial para programação assíncrona e concorrência. O asyncio permite escrever código que executa múltiplas tarefas de I/O de forma eficiente, usando uma única thread e um loop de eventos. Vamos explorar conceitos práticos como criação de tarefas, agregação de corrotinas, controle de tempo e operações de I/O assíncrono, com exemplos que você pode executar e adaptar.
Dominar asyncio é fundamental para desenvolver aplicações web de alta performance, clientes de rede, ferramentas de automação e qualquer sistema que precise lidar com muitas operações de entrada/saída simultaneamente. Ao final desta aula, você terá uma base sólida para usar asyncio em seus projetos.
Tarefas (create_task)
Em asyncio, uma corrotina é uma função definida com async def que pode ser pausada e retomada. Para executar uma corrotina de forma concorrente com outras, usamos asyncio.create_task(), que agenda a execução da corrotina no loop de eventos e retorna um objeto Task. Isso permite que várias corrotinas sejam executadas de forma intercalada, sem bloquear o loop.
Vejamos um exemplo simples:
import asyncio
async def dizer_oi(nome, delay):
await asyncio.sleep(delay)
print(f"Olá, {nome}!")
async def main():
# Cria duas tarefas que rodam concorrentemente
tarefa1 = asyncio.create_task(dizer_oi("Alice", 2))
tarefa2 = asyncio.create_task(dizer_oi("Bob", 1))
# Aguarda ambas terminarem
await tarefa1
await tarefa2
asyncio.run(main())
Ao executar, a mensagem de Bob aparece primeiro (após 1 segundo) e a de Alice depois (após 2 segundos), mostrando a execução concorrente. Note que usamos await tarefa para aguardar a conclusão. Se não aguardarmos, a tarefa pode ser cancelada quando o programa terminar.
Uma prática comum é aguardar várias tarefas com asyncio.gather(), que veremos a seguir. Também é importante lembrar que create_task só funciona dentro de uma corrotina em execução (ou seja, dentro de async).
gather
asyncio.gather() é uma função que agrega várias corrotinas ou tarefas e as executa de forma concorrente, retornando uma lista com os resultados na mesma ordem em que foram passadas. É ideal quando você precisa executar várias operações assíncronas e coletar seus resultados.
Exemplo:
import asyncio
async def calcular_quadrado(n):
await asyncio.sleep(1) # simula operação demorada
return n * n
async def main():
resultados = await asyncio.gather(
calcular_quadrado(2),
calcular_quadrado(3),
calcular_quadrado(4),
)
print(resultados) # [4, 9, 16]
asyncio.run(main())
Se uma das corrotinas lançar uma exceção, gather propaga a primeira exceção encontrada, cancelando as demais (por padrão). Para evitar isso, use return_exceptions=True, que retorna as exceções como objetos na lista de resultados.
async def falhar():
raise ValueError("Erro!")
async def main():
resultados = await asyncio.gather(
falhar(),
calcular_quadrado(2),
return_exceptions=True,
)
print(resultados) # [ValueError('Erro!'), 4]
Isso é útil para lidar com falhas parciais sem interromper todas as tarefas.
Timeouts
Em muitas aplicações, é necessário limitar o tempo de execução de uma operação assíncrona. O asyncio oferece asyncio.timeout() (Python 3.11+) e asyncio.wait_for() para esse fim. wait_for aguarda uma corrotina ou tarefa e lança asyncio.TimeoutError se o tempo limite for excedido.
Exemplo com wait_for:
import asyncio
async def operacao_demorada():
await asyncio.sleep(10)
return "Concluído"
async def main():
try:
resultado = await asyncio.wait_for(operacao_demorada(), timeout=2)
print(resultado)
except asyncio.TimeoutError:
print("A operação excedeu o tempo limite!")
asyncio.run(main())
Já em Python 3.11+, a forma recomendada é usar asyncio.timeout() como um gerenciador de contexto assíncrono:
import asyncio
async def main():
try:
async with asyncio.timeout(2):
await operacao_demorada()
except TimeoutError:
print("Tempo esgotado!")
Note que asyncio.timeout() lança TimeoutError (não asyncio.TimeoutError). O uso de timeout é mais limpo e integra-se bem com outras funcionalidades, como cancelamento.
É importante tratar os timeouts adequadamente para não deixar recursos pendentes ou tarefas órfãs.
I/O assíncrono
O asyncio é especialmente útil para operações de I/O, como leitura/escrita de arquivos, comunicação de rede e chamadas HTTP. Vamos ver um exemplo de leitura de arquivos de forma assíncrona usando a biblioteca aiofiles (que precisa ser instalada: pip install aiofiles).
import asyncio
import aiofiles
async def ler_arquivo(caminho):
async with aiofiles.open(caminho, mode='r') as f:
conteudo = await f.read()
return conteudo
async def main():
conteudo = await ler_arquivo('exemplo.txt')
print(conteudo[:100]) # imprime os primeiros 100 caracteres
asyncio.run(main())
Da mesma forma, para requisições HTTP assíncronas, usamos bibliotecas como aiohttp ou httpx. Exemplo com httpx (instale com pip install httpx):
import asyncio
import httpx
async def buscar_url(url):
async with httpx.AsyncClient() as cliente:
resposta = await cliente.get(url)
return resposta.status_code
async def main():
urls = ['https://example.com', 'https://httpbin.org/get']
statuses = await asyncio.gather(*(buscar_url(url) for url in urls))
print(statuses)
asyncio.run(main())
Essas operações não bloqueiam o loop de eventos, permitindo que outras tarefas sejam executadas enquanto a operação de I/O está pendente. Isso é fundamental para criar aplicações escaláveis.
Boas práticas e observações finais
Ao trabalhar com asyncio, tenha em mente:
- Sempre use
asyncio.run()como ponto de entrada, exceto em contextos onde o loop já está rodando (como em frameworks web). - Evite misturar código síncrono que bloqueia (como
time.sleep) dentro de corrotinas; prefiraasyncio.sleep. - Para tarefas de longa duração, considere usar
asyncio.create_taske aguardar comgather. - Use
asyncio.timeoutpara controlar operações que podem travar. - Em aplicações reais, use bibliotecas assíncronas para I/O (aiofiles, aiohttp, asyncpg, etc.) para aproveitar todo o potencial do asyncio.
Exercícios
-
Escreva uma corrotina
contar(nome, atraso)que imprime o nome, esperaatrasosegundos e imprime o nome novamente. Usecreate_taskpara executar duas dessas corrotinas com atrasos diferentes emmaine aguarde ambas.✓ Resposta:import asyncio async def contar(nome, atraso): print(f"{nome} iniciou") await asyncio.sleep(atraso) print(f"{nome} terminou") async def main(): t1 = asyncio.create_task(contar("A", 2)) t2 = asyncio.create_task(contar("B", 1)) await t1 await t2 asyncio.run(main()) -
Crie uma função assíncrona
soma(a, b)que retorna a soma após 1 segundo. Useasyncio.gatherpara somar três pares de números e imprimir os resultados.✓ Resposta:import asyncio async def soma(a, b): await asyncio.sleep(1) return a + b async def main(): resultados = await asyncio.gather( soma(1, 2), soma(3, 4), soma(5, 6), ) print(resultados) # [3, 7, 11] asyncio.run(main()) -
Usando
asyncio.wait_for, crie uma corrotina que dorme 5 segundos e ummainque espera 2 segundos. Capture a exceção e imprima uma mensagem.✓ Resposta:import asyncio async def dormir(): await asyncio.sleep(5) return "acordou" async def main(): try: resultado = await asyncio.wait_for(dormir(), timeout=2) print(resultado) except asyncio.TimeoutError: print("A operação demorou demais!") asyncio.run(main()) -
Escreva um programa que use
aiohttppara fazer requisições GET a duas URLs diferentes simultaneamente e imprima o status de cada uma. (Assuma queaiohttpestá instalado.)✓ Resposta:import asyncio import aiohttp async def buscar_status(url): async with aiohttp.ClientSession() as sessao: async with sessao.get(url) as resp: return resp.status async def main(): urls = ['https://example.com', 'https://httpbin.org/get'] statuses = await asyncio.gather(*(buscar_status(url) for url in urls)) print(statuses) asyncio.run(main()) -
Explique a diferença entre
asyncio.create_taskeasyncio.gathere quando usar cada um.✓ Resposta:create_taskagenda uma única corrotina para execução concorrente, retornando um objetoTaskque pode ser aguardado individualmente.gatherrecebe várias corrotinas/tarefas, executa-as concorrentemente e retorna uma lista de resultados. Usecreate_taskquando você precisa de controle individual sobre cada tarefa (por exemplo, cancelar uma delas) ou quando quer aguardar tarefas em momentos diferentes. Usegatherquando você quer executar um conjunto de corrotinas e aguardar todas juntas, coletando os resultados.