5.2 · Programación asíncrona (asyncio)

Nivel 5 · Nivel pro e industrialización

5.2Programación asíncrona (asyncio)

Objetivo: Gestionar la concurrencia con async/await y asyncio.
Tiempo estimado: 15 min · lectura autónoma · Si solo tienes 5 min: lee «Lo esencial en 30s» + el esquema.

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.

Error frecuente de interpretación: « El async lo hace todo más rápido, y requests funciona dentro ». No. El async solo acelera las cargas dominadas por la espera (I/O-bound) ; en cálculo puro, complica sin ganancia. Y requests, síncrono y bloqueante, paraliza el bucle : en el async, se usan libs async-compatibles (httpx/aiohttp) — si no, se ha pagado la complejidad sin tocar la ganancia.
Esperar juntos Declarar, esperar sin bloquear, lanzar juntos async def Declararla corrutinaawait Esperar sinbloqueargather Lanzar juntos
Declarar, esperar sin bloquear, lanzar juntos
Versión en texto del esquema (SEO / lectores de pantalla / traducción): 1) async def : Nabil declara su recogida de datos en Argel en corrutinas. 2) await : cada espera de red libera el hilo en lugar de bloquearlo. 3) gather : todas las recogidas salen juntas — las esperas se solapan. Lección : libs async-compatibles de principio a fin, y medida de la ganancia antes/después.

¿Lo sabíais?

Olvidar un await crea una corrutina fantasma : existe pero nunca se ejecuta — Python avisa con una discreta advertencia que muchos ignoran.
En async, el olvido no falla : simplemente no pasa nada.

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

  1. Escribid una corrutina « async def tache(n) » que muestre un mensaje, espere (await asyncio.sleep(1)) y luego muestre el fin.
  2. Lanzadla con asyncio.run() y observad el comportamiento secuencial de una sola tarea.
  3. Lanzad varias tareas EN PARALELO con asyncio.gather() : constataréis que 3 esperas de 1 s tardan ~1 s en total, no 3.
  4. Concluid : lo asíncrono brilla cuando se ESPERA (red, I/O), no para acelerar un cálculo intensivo.
Comprendéis async/await y asyncio, y sabéis cuándo lo asíncrono aporta una ganancia real (esperas I/O) : una competencia avanzada buscada.

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?

Lista de salida de la sección
  • 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.
Autoevaluación 1-5 : ¿he distinguido espera y cálculo antes de codificar?¿queda una llamada bloqueante en mi cadena 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.

Comprueba tu comprensión

¿En qué caso asyncio aporta una ganancia?

Recursos adicionales (opcional, para ir más allá)

Examen del nivel 5Preguntas inéditas y más difíciles para validar tus conocimientos