5.2Programación asíncrona (asyncio)
Lo esencial en 30 segundos
- Async = explotar la espera : mientras espera (API, base, archivos), el programa hace otra cosa en lugar de quedarse bloqueado.
- El trío : async def declara, await espera sin bloquear, gather lanza juntos — asyncio.run en la cima.
- Mundos separados : lo bloqueante (requests, sleep) paraliza el async — libs async-compatibles o nada.
Mini-caso hilado — Nabil acelera sus recogidas en Argel
Nabil, analista de datos junior en Argel, recupera cada mañana datos desde varias fuentes web : en secuencial, su script espera cada respuesta una tras otra y la recogida dura una eternidad. Reescribe el bucle en corrutinas lanzadas juntas con asyncio.gather : las esperas se solapan en lugar de sumarse. Continuación del caso 5.1 — Nabil ocupa los tiempos muertos en lugar de sufrirlos.
Continuación del caso 5.1 — Nabil pasa de lo secuencial que espera a lo concurrente que solapa.
1. Cuando la espera domina : el async
API, base de datos, archivos remotos : el programa espera más de lo que calcula. La programación asíncrona libera el hilo durante la espera : en lugar de quedarse bloqueado, pasa a otras tareas, y un solo hilo gestiona miles de peticiones concurrentes sin miles de hilos. Nabil reencuentra su recogida matinal : el cuello de botella es la espera de red, no el cálculo.
Frontera nítida : para cálculo puro (CPU-bound), el async no acelera nada — es multiprocessing. Async para la I/O, multiprocessing para la CPU : elegir según el cuello de botella, no según la moda.
2. El trío : async def, await, gather
async def fetch(url) declara una corrutina, await espera sin bloquear (el hilo se libera para otras tareas), asyncio.gather(*tareas) lanza juntas y espera a todas. asyncio.run(main()) orquesta desde la cima vía el bucle de eventos. Nabil lanza sus recogidas en gather : tres esperas se solapan en lugar de sumarse.
Trampa clásica : olvidar un await deja una corrutina nunca ejecutada (advertencia discreta, resultado vacío). Cada corrutina creada debe esperarse o reunirse.
3. No mezclar los mundos
Lo bloqueante en el async paraliza todo : time.sleep congela el hilo, requests (síncrono) anula el beneficio — usad asyncio.sleep y libs async-compatibles (httpx, aiohttp). Un resto bloqueante superviviente se aísla vía run_in_executor. Nabil sustituye requests por un cliente async : sin eso, su gather seguía esperando.
Regla de oro : async de principio a fin o nada — lo mixto es lo peor de los mundos. Y se mide antes/después para probar la ganancia en lugar de suponerla.
¿Lo sabíais?
Corrutina creada, nunca esperada : trabajo prometido, nunca hecho.
🔎 Ejemplo concreto — Nabil recoge sus fuentes en paralelo
Las esperas se solapan en lugar de sumarse.
import asyncio
async def collecter(source):
print(f"collecte {source}...")
await asyncio.sleep(1)
return f"{source} : ok"
async def main(sources):
return await asyncio.gather(*(collecter(s) for s in sources))
res = asyncio.run(main(["api", "fichier", "base"]))
print(res)
Lectura : tres esperas de un segundo tardan cerca de un segundo en total, no tres.
Lección : en producción de red, sustituid el sleep por un cliente async (httpx/aiohttp).
Vocabulario de la sección
- Programación asíncrona
- Modelo que ocupa el programa durante los tiempos de espera en lugar de bloquearlo.
Ejemplo sencillo: Nabil recoge tres fuentes a la vez en lugar de esperar cada una.
Lo que no es: varios procesadores que calculan en paralelo.
Reflejo: ¿mi programa espera más de lo que calcula?
- async / await
- async declara una corrutina ; await espera sin bloquear el hilo.
Ejemplo sencillo: Nabil espera (await) cada recogida para liberar el hilo.
Lo que no es: una llamada bloqueante que lo congela todo.
Reflejo: ¿he olvidado un await (corrutina fantasma)?
- asyncio.gather
- Lanza varias corrutinas juntas y espera a todas.
Ejemplo sencillo: Nabil reúne sus tres recogidas en un solo gather.
Lo que no es: un bucle que espera cada tarea una tras otra.
Reflejo: ¿estas esperas pueden solaparse?
- Corrutina
- Función async suspendible y reanudable por el bucle de eventos.
Ejemplo sencillo: La función de recogida de Nabil, suspendida durante el sleep.
Lo que no es: una función normal que bloquea hasta el final.
Reflejo: ¿quién espera esta corrutina?
- I/O-bound vs CPU-bound
- Espera (red/disco) → asyncio ; cálculo intensivo → multiprocessing.
Ejemplo sencillo: Nabil elige asyncio para sus API, no para un gran cálculo.
Lo que no es: async para acelerar un cálculo puro.
Reflejo: ¿mi cuello de botella es la espera o el cálculo?
En la práctica — Descubrir async/await con asyncio
- Escribid una corrutina « async def tache(n) » que muestre un mensaje, espere (await asyncio.sleep(1)) y luego muestre el fin.
- Lanzadla con asyncio.run() y observad el comportamiento secuencial de una sola tarea.
- Lanzad varias tareas EN PARALELO con asyncio.gather() : constataréis que 3 esperas de 1 s tardan ~1 s en total, no 3.
- Concluid : lo asíncrono brilla cuando se ESPERA (red, I/O), no para acelerar un cálculo intensivo.
Guion listo para decir — justificar el async
« En Argel, nuestra recogida matinal espera cada fuente una tras otra : el cuello de botella es la espera de red, no el cálculo. Propongo pasar el bucle a corrutinas con gather y un cliente async : las esperas se solaparán. Mido antes/después para probar la ganancia, y dejo el cálculo puro fuera del async. »
Para decir al responsable para validar una reescritura dirigida, medida, sin tocar el cálculo.
Modelo copiar-pegar — recogida async genérica
import asyncio
async def tache(nom):
print(f"debut {nom}")
await asyncio.sleep(1)
print(f"fin {nom}")
return nom
async def main():
return await asyncio.gather(*(tache(n) for n in ["a", "b", "c"]))
if __name__ == "__main__":
print(asyncio.run(main()))
# En production reseau : remplacer asyncio.sleep par httpx/aiohttp (async-compatibles).
Solo / presupuesto pequeño
Nabil solo : gather sobre sus fuentes existentes basta — sin nueva infra, solo corrutinas.
PYME / equipo reducido
Cliente HTTP async compartido : todo el equipo recoge sin multiplicar las sesiones bloqueantes.
PYME en Argel
Nabil mide antes/después en Argel sobre la recogida real : la ganancia probada justifica la reescritura.
Equipo que empieza
Pareja : uno convierte una fuente en async, el otro rastrea las llamadas bloqueantes restantes.
✓ Buenas prácticas
- Async para la I/O, multiprocessing para la CPU : elegir según el cuello de botella.
- gather para lanzar juntos ; libs async-compatibles (httpx).
- Medir antes/después : probar la ganancia en lugar de suponerla.
✗ Errores frecuentes
- Llamar requests (bloqueante) en el async.
- Olvidar un await : corrutina fantasma nunca ejecutada.
- Poner async sobre cálculo puro (ninguna ganancia, complejidad regalada).
Puntos clave
- Lo asíncrono ocupa el programa durante los tiempos de espera en lugar de bloquearlo.
- async define una corrutina ; await pausa esperando un resultado.
- asyncio.gather() lanza varias tareas de espera en paralelo.
- asyncio sirve para los casos I/O-bound (red, disco), no para los cálculos intensivos (→ multiprocessing).
Reto 48h — aplica mañana
Convertid 1 bucle de espera real (API, archivos) en gather : ¿las 3 esperas siguen tardando 3 veces el tiempo?
- Puedo explicar async/await/gather en una frase.
- Mi demo gather solapa las esperas (prueba medida).
- Sé por qué requests está prohibido en el async.
Preguntas frecuentes
Asíncrono, hilos, multiprocessing : ¿qué diferencia?
Tres respuestas a tres cuellos de botella. asyncio gestiona la concurrencia de esperas I/O en un solo hilo (ideal red/disco). El threading comparte también un proceso pero conviene a ciertos I/O bloqueantes. El multiprocessing lanza varios procesos para paralelizar el cálculo en varios núcleos. Se elige según que el cuello de botella sea la espera (asyncio) o el cálculo (multiprocessing).
¿Lo asíncrono hace forzosamente un programa más rápido?
No. Acelera las cargas dominadas por la espera (muchas peticiones de red simultáneas). Para un cálculo puro (CPU-bound), no aporta nada, e incluso complica inútilmente el código. Usadlo con criterio, donde haya tiempos muertos por explotar — y medid para probarlo.
¿En qué caso asyncio aporta una ganancia?