Python Async vs Threads: Quando Usar asyncio
Por Que a Concorrência Importa no Python
Você tem um script Python que busca dados de múltiplas APIs. Ele funciona, mas é lento — cada requisição espera a anterior terminar. Você já ouviu falar sobre asyncio e threads, mas qual usar? Escolher o modelo errado pode levar a um código desnecessariamente complexo ou dolorosamente lento.
Este guia esclarece a confusão. Vamos comparar os modelos async e de threading do Python, mostrar exatamente quando usar cada um e fornecer exemplos práticos que você pode adaptar aos seus próprios projetos.
Entendendo o Cenário de Concorrência do Python
O Python oferece três modelos principais de concorrência:
- Threads – Múltiplas threads de execução dentro de um único processo, gerenciadas pelo SO. Limitadas pelo Global Interpreter Lock (GIL) para trabalho CPU-bound.
- Async (asyncio) – Event loop single-threaded que alterna entre tarefas em pontos
await. Excelente para operações I/O-bound. - Multiprocessing – Processos separados, cada um com seu próprio interpretador Python e espaço de memória. Contorna o GIL para paralelismo real.
O GIL é um mutex que impede múltiplas threads de executar bytecode Python simultaneamente. Isso significa que threads não aceleram tarefas CPU-bound, mas ajudam em tarefas I/O-bound porque o GIL é liberado durante operações de I/O.
Quando Usar asyncio
asyncio brilha quando seu programa passa a maior parte do tempo esperando por eventos externos: requisições de rede, I/O de arquivos, consultas a banco de dados ou temporizadores. Como usa uma única thread, você evita a sobrecarga de criação de threads e troca de contexto. O event loop gerencia eficientemente milhares de conexões simultâneas.
Use asyncio quando:
- Você está construindo um servidor ou cliente web que lida com muitas conexões simultâneas (ex.: FastAPI, aiohttp).
- Suas tarefas são I/O-bound e você precisa de alta concorrência (centenas ou milhares de tarefas).
- Você quer controle refinado sobre agendamento e cancelamento de tarefas.
- Você já está usando um framework ou biblioteca async.
Aqui está um exemplo simples que busca múltiplas URLs concorrentemente:
import asyncio
import aiohttp
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://example.com',
'https://example.org',
'https://example.net',
]
tasks = [fetch(url) for url in urls]
results = await asyncio.gather(*tasks)
print(f'Fetched {len(results)} pages')
asyncio.run(main())
Quando Usar Threads
Threads são adequadas quando você tem operações de I/O bloqueantes que não possuem equivalentes async, ou quando está trabalhando com código legado que não é compatível com async. Elas também são mais simples de raciocinar para um pequeno número de tarefas.
Use threads quando:
- Você precisa executar I/O bloqueante em paralelo, como ler múltiplos arquivos ou fazer chamadas síncronas a APIs.
- Você está integrando com uma biblioteca que não suporta async.
- Você tem um número moderado de tarefas concorrentes (dezenas, não milhares).
- Você quer manter seu código síncrono, mas ainda sobrepor I/O.
Exemplo usando concurrent.futures.ThreadPoolExecutor:
from concurrent.futures import ThreadPoolExecutor
import requests
def fetch(url):
response = requests.get(url)
return response.text
urls = [
'https://example.com',
'https://example.org',
'https://example.net',
]
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(fetch, urls))
print(f'Fetched {len(results)} pages')
Comparando asyncio e Threads
Aqui está uma comparação lado a lado para ajudar você a decidir:
| Aspecto | asyncio | Threads |
|---|---|---|
| Modelo de concorrência | Event loop single-threaded | Múltiplas threads do SO |
| Melhor para | I/O-bound, alta concorrência | I/O-bound, chamadas bloqueantes |
| Desempenho CPU-bound | Ruim (GIL) | Ruim (GIL) |
| Escalabilidade | Milhares de tarefas | Centenas de threads |
| Complexidade | Requer sintaxe async/await | Estilo síncrono familiar |
| Depuração | Pode ser complicada com tracebacks | Ferramentas de depuração padrão |
| Ecossistema | Suporte async crescente | Universal |
E as Tarefas CPU-Bound?
Nem asyncio nem threads ajudarão a paralelizar trabalho intensivo de CPU em Python devido ao GIL. Para tarefas CPU-bound — como cálculos numéricos, processamento de imagens ou compressão de dados — use multiprocessing ou concurrent.futures.ProcessPoolExecutor.
Exemplo:
from concurrent.futures import ProcessPoolExecutor
def cpu_heavy(n):
return sum(i * i for i in range(n))
if __name__ == '__main__':
with ProcessPoolExecutor() as executor:
results = list(executor.map(cpu_heavy, [10**6, 10**6, 10**6]))
print(results)
Boas Práticas e Armadilhas
- Não misture chamadas bloqueantes com asyncio. Se você chamar uma função bloqueante dentro de uma tarefa async, ela bloqueará todo o event loop. Use
asyncio.to_thread()para executar código bloqueante em um pool de threads. - Limite o tamanho do pool de threads. Criar muitas threads leva a sobrecarga de troca de contexto. Use
ThreadPoolExecutor(max_workers=N). - Trate exceções adequadamente. No asyncio, exceções não tratadas em tarefas podem passar despercebidas. Use
asyncio.gather(..., return_exceptions=True)ou adicione callbacks. - Prefira asyncio para novos projetos com muito I/O. O ecossistema é maduro, e frameworks como FastAPI e aiohttp facilitam.
- Use threads para scripts legados ou simples. Se você só precisa paralelizar algumas chamadas bloqueantes, threads são menos invasivas.
FAQ
Posso usar asyncio e threads juntos?
Sim. Você pode executar código bloqueante em um pool de threads usando asyncio.to_thread() ou loop.run_in_executor(). Isso é útil quando você precisa chamar uma biblioteca síncrona a partir de código async sem bloquear o event loop.
O asyncio funciona com o GIL?
Sim, o asyncio roda em uma única thread e está sujeito ao GIL. No entanto, como é projetado para tarefas I/O-bound, o GIL é liberado durante operações de I/O, permitindo que outras tarefas rodem. Para trabalho CPU-bound, o asyncio não oferece ganho de velocidade.
Qual é mais rápido: asyncio ou threads?
Para tarefas I/O-bound com muitas operações concorrentes, asyncio é geralmente mais rápido e escalável porque evita a sobrecarga de threads. Para um pequeno número de tarefas bloqueantes, threads podem ser mais simples e ter desempenho similar. Nenhum dos dois ajuda com tarefas CPU-bound.
Fazendo a Escolha Certa
Comece identificando se seu gargalo é I/O ou CPU. Se for I/O e você precisa de alta concorrência, use asyncio. Se for I/O, mas você está lidando com bibliotecas bloqueantes ou scripts mais simples, threads são uma escolha sólida. Para trabalho CPU-bound, use multiprocessing.
Lembre-se de que você pode frequentemente combinar esses modelos — por exemplo, usando asyncio para operações de rede e um pool de threads para I/O de arquivos. O segredo é entender os trade-offs e escolher a ferramenta que se adequa ao seu problema específico.
Quando você precisar formatar ou validar rapidamente dados JSON retornados de suas chamadas de API concorrentes, experimente nosso JSON Formatter para formatar e depurar com facilidade.