asyncio es concurrencia en un solo hilo: un event loop que coordina miles de tareas que esperan I/O (red, disco). Es el motor detrás de FastAPI, aiohttp y todo el ecosistema asíncrono de Python.
La idea: nunca esperar ocioso
Imagina un mesero (el event loop) y diez mesas (tus tareas). Un mesero síncrono atiende una mesa de principio a fin. El mesero asíncrono toma la orden de la mesa 1, la pasa a la cocina (await), y mientras la cocina trabaja, atiende la mesa 2. Nunca está parado esperando.
async def y await
import asyncio
async def preparar_cafe(): # esto es una CORRUTINA
print("moliendo...")
await asyncio.sleep(2) # pausa NO bloqueante: cede el control
print("café listo")
return "espresso"
async def main():
resultado = await preparar_cafe() # esperar sin bloquear el loop
print(resultado)
asyncio.run(main()) # el punto de entrada: arranca el event loop
Detalles que confunden a todos al principio:
- Llamar
preparar_cafe()no ejecuta nada: devuelve un objeto coroutine awaites el que la ejecuta (y pausa hasta que termine)await asyncio.sleep(2)simula espera — a diferencia detime.sleep(2), que bloquearía todo el loop- Solo puedes
awaitdentro de funcionesasync def
Concurrencia real: gather
import asyncio
async def tarea(nombre, segundos):
await asyncio.sleep(segundos)
print(f"{nombre} terminó")
return nombre.upper()
async def main():
inicio = asyncio.get_event_loop().time()
resultados = await asyncio.gather(
tarea("A", 2),
tarea("B", 1),
tarea("C", 3),
)
total = asyncio.get_event_loop().time() - inicio
print(resultados) # ['A', 'B', 'C'] → en orden de llamada
print(f"{total:.0f}s") # ~3s (la más larga), NO 6s
asyncio.run(main())
gather corre las tres tareas concurrentemente: 2+1+3 segundos de espera se solapan y el total es el de la más lenta. Esto es la magia de asyncio.
La regla de oro: no bloquear el loop
import time
async def mal():
time.sleep(5) # BLOQUEA todo el loop: ninguna tarea avanza
async def bien():
await asyncio.sleep(5) # cede el control: otras tareas corren
Una sola llamada bloqueante (time.sleep, requests, cálculo pesado) congela TODAS tus tareas. Para librerías de I/O usa las versiones async (aiohttp, no requests).
Tareas: el nivel siguiente
asyncio.create_task lanza una corrutina "en el fondo" sin esperarla de inmediato:
async def main():
tarea = asyncio.create_task(tarea("X", 2)) # empieza YA
print("hago otras cosas mientras...") # corre mientras X espera
resultado = await tarea # aquí sí espero el resultado
print(resultado)
Y para trabajo pesado de CPU dentro de un mundo async: await asyncio.to_thread(funcion_bloqueante) lo manda a un hilo.
¿asyncio o threading?
| Criterio | asyncio | threading |
|---|---|---|
| Miles de conexiones | Excelente (muy ligero) | Inviable (miles de hilos) |
| Código de librerías | Necesita ecosistema async | Usa librerías normales |
| Complejidad mental | Alta al inicio | Riesgos de carrera |
Para servicios web con muchas conexiones lentas (APIs, websockets), asyncio es el rey. Es el modelo mental de FastAPI.
Resumen
async defdefine corrutinas;awaitlas ejecuta cediendo el controlasyncio.run(main())es el punto de entradagathercorre tareas concurrentes: el total es la tarea más lenta, no la suma- Nunca bloquees el loop:
asyncio.sleep, notime.sleep; librerías async, no bloqueantes - asyncio brilla con MUCHAS esperas de I/O; para CPU, procesos