Async y Concurrencia para Rendimiento
Los modelos de concurrencia de Python se dirigen a diferentes cuellos de botella: asyncio para paralelismo de I/O, multiprocessing para paralelismo de CPU y threading para I/O con bibliotecas bloqueantes.
Receta
import asyncio
import httpx
async def fetch_all(urls: list[str]) -> list[int]:
async with httpx.AsyncClient() as client:
responses = await asyncio.gather(*[client.get(u) for u in urls])
return [r.status_code for r in responses]Cuándo usar esto:
- Muchas solicitudes HTTP/base de datos concurrentes (limitado por I/O)
- Trabajo limitado por CPU en múltiples núcleos (multiprocesamiento)
- El rendimiento importa más que la latencia de una sola solicitud
Ejemplo de Trabajo
import asyncio
import httpx
from concurrent.futures import ProcessPoolExecutor
# Limitado por I/O: asyncio
async def fetch_urls(urls: list[str]) -> list[str]:
async with httpx.AsyncClient(timeout=10.0) as client:
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks, return_exceptions=True)
return [r.text[:100] if isinstance(r, httpx.Response) else str(r) for r in responses]
# Limitado por CPU: pool de procesos
def compute(n: int) -> int:
return sum(i * i for i in range(n))
async def run_cpu_bound(items: list[int]) -> list[int]:
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
return list(await asyncio.gather(*[
loop.run_in_executor(pool, compute, n) for n in items
]))
async def main():
urls = ["https://httpbin.org/get"] * 20
io_results = await fetch_urls(urls)
cpu_results = await run_cpu_bound([100_000] * 4)
print(len(io_results), sum(cpu_results))
asyncio.run(main())Lo que esto demuestra:
asyncio.gatherejecuta I/O concurrentemente en un hiloProcessPoolExecutorevita el GIL para el trabajo de CPUreturn_exceptions=Trueevita que un fallo cancele todos los demás- Combina modelos: orquestación asíncrona + pool de procesos para etapas de CPU
Inmersión Profunda
Selección de Modelo
| Cuello de botella | Modelo | Herramienta |
|---|---|---|
| I/O (red, disco) | asyncio | httpx, asyncpg |
| CPU | multiprocessing | ProcessPoolExecutor |
| Biblioteca de I/O bloqueante | threading | ThreadPoolExecutor |
| Mixto | asyncio + executor | run_in_executor |
Errores Comunes
- asyncio para trabajo de CPU - sin mejora de velocidad, añade complejidad. Solución: multiprocessing o vectorización.
- Concurrencia ilimitada - satura el servidor o alcanza límites de conexión. Solución:
asyncio.Semaphore(50)para limitar las solicitudes paralelas. - Estado mutable compartido entre hilos - condiciones de carrera. Solución: colas, locks o aislamiento de procesos.
- Crear procesos por tarea - sobrecarga de
fork. Solución: reutilizarProcessPoolExecutor. - Llamada bloqueante en función asíncrona - congela el bucle de eventos. Solución:
await asyncio.to_thread(blocking_fn)o biblioteca asíncrona.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Sync + threading | Código bloqueante heredado | Greenfield asíncrono |
| Celery/RQ | Cola de tareas distribuida | Paralelismo en proceso suficiente |
| Escalado horizontal | Rendimiento en producción | Fase de optimización local |
Preguntas Frecuentes
¿asyncio o hilos?
asyncio para bibliotecas asíncronas nativas. Hilos para bibliotecas bloqueantes que no puedes reemplazar.
¿Cuántas solicitudes concurrentes?
Empieza con 10-50. Ajusta con un semáforo. Vigila los límites del servidor y del cliente.
¿asyncio evita el GIL?
No. asyncio multiplexa I/O en un solo hilo. El paralelismo de CPU necesita multiprocessing.
¿FastAPI y asyncio?
FastAPI es nativo asíncrono. Usa drivers de base de datos asíncronos para obtener el máximo beneficio.
¿Cómo limito la tasa?
asyncio.Semaphore o bucket de tokens con asyncio.sleep.
¿Tamaño del pool de procesos?
Típicamente os.cpu_count(). Los pools de I/O pueden ser más grandes que el número de CPU.
¿asyncio.gather vs TaskGroup?
Python 3.14: prefiere asyncio.TaskGroup para concurrencia estructurada y manejo de errores.
¿Cómo mido el rendimiento?
Solicitudes por segundo bajo carga (locust, hey, k6). No timeit de solicitud única.
¿Seguridad de hilos con free-threaded 3.14?
Audita el estado mutable compartido. Usa locks o estructuras de datos inmutables.
¿Cuándo es suficiente sync?
Bajo tráfico, pocas operaciones de I/O concurrentes, o trabajos por lotes sin requisitos de latencia.
Relacionado
- 30 Reglas de Async - trampas de asyncio
- Concurrencia - GIL y modelos
- Caché y Memoización - reduce I/O
- Mejores Prácticas de Rendimiento - selección de modelo
Versiones de la Pila: Esta página fue escrita para Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.