7 - Asincronía con asyncio

asyncio permite escribir código concurrente basado en un bucle de eventos y corrutinas cooperativas. Ideal para tareas I/O-bound con muchas conexiones simultáneas sin crear miles de hilos.
No ofrece paralelismo de CPU; sirve para solapar esperas de red, disco o timers usando un único hilo.

7.1 Conceptos clave

  • Event loop: orquesta la ejecución de corrutinas y callbacks.
  • Corrutinas: funciones declaradas con async def que ceden control con await.
  • Tareas: envoltorios de corrutinas que el loop puede planificar (asyncio.create_task).

7.2 Esperar I/O sin bloquear

Las funciones de asyncio liberan el loop mientras esperan sockets, timers o disco. Usa siempre await en operaciones I/O async (por ejemplo asyncio.sleep, aiohttp).
Si llamas APIs sin await, el trabajo no se ejecuta y perderás errores.

7.3 Lanzar muchas corrutinas

  • asyncio.gather: ejecuta varias corrutinas en paralelo y espera a todas.
    Usa return_exceptions=True si quieres recolectar errores sin abortar.
  • asyncio.create_task: programa una corrutina y devuelve una tarea que puedes monitorear o cancelar.
    Guarda la referencia para poder cancelar en shutdown ordenado.

7.4 Código bloqueante en ejecutores

Si necesitas llamar a funciones bloqueantes (CPU o librerías sync), usa loop.run_in_executor para no congelar el bucle. Para CPU-bound considera ProcessPoolExecutor; para I/O sync, ThreadPoolExecutor.
Nunca llames directamente a código bloqueante en una corrutina principal: atascarás todo el loop.

7.5 Errores comunes

  • Olvidar await: la corrutina no se ejecuta y el linter suele avisar.
    Cualquier advertencia "coroutine was never awaited" es una fuga de trabajo.
  • Bloquear el loop con operaciones sync pesadas.
    Muévelas a un executor o usa procesos.
  • Crear demasiadas tareas sin límite: usa semáforos o colas.
    Controla también timeouts y cancela tareas en el cierre.

7.6 Aplicación: Descargador async con límite de concurrencia

Ejemplo sencillo con asyncio y aiohttp (pip install aiohttp) que limita el número de descargas simultáneas.
Incluye manejo básico de timeout y cancelación ordenada.

import asyncio
import aiohttp

URLS = [
    "https://www.example.com/",
    "https://www.python.org/",
    "https://www.infobae.com/"
]


async def fetch(session, url, sem):
    async with sem:
        async with session.get(url, timeout=5) as resp:
            await resp.text()
            print(f"Listo: {url} -> {resp.status}")


async def main():
    sem = asyncio.Semaphore(5)  # límite de concurrencia
    async with aiohttp.ClientSession() as session:
        tareas = [asyncio.create_task(fetch(session, url, sem)) for url in URLS]
        try:
            await asyncio.gather(*tareas)
        except Exception as exc:
            print(f"Alguna tarea fallo: {exc}")
            for t in tareas:
                t.cancel()
            await asyncio.gather(*tareas, return_exceptions=True)


if __name__ == "__main__":
    asyncio.run(main())

El semáforo evita lanzar más solicitudes simultáneas de las soportadas. asyncio.run gestiona el event loop y asegura un cierre ordenado de recursos.