Programación Asíncrona con asyncio
En aplicaciones modernas que realizan muchas operaciones de red o entrada/salida (I/O), como consultar APIs, descargar archivos o interactuar con bases de datos, esperar de forma síncrona a que cada operación termine bloquea el hilo principal y desperdicia recursos.
asyncio es el módulo estándar de Python para escribir programas concurrentes utilizando la sintaxis async / await basada en un Bucle de Eventos (Event Loop).
1. El Bucle de Eventos (Event Loop) y Corrutinas #
- Corrutina: Una función declarada con
async def. Al llamarla no se ejecuta inmediatamente, sino que devuelve un objeto corrutina. await: Pausa la ejecución de la corrutina actual cediendo el control al bucle de eventos para que otras tareas avancen mientras se espera una operación asíncrona.asyncio.run(): Punto de entrada para iniciar el bucle de eventos.
1import asyncio 2 3async def consultar_servicio(nombre: str, segundos: float) -> str: 4 print(f"[{nombre}] Iniciando consulta...") 5 await asyncio.sleep(segundos) # Simula una operación I/O asíncrona no bloqueante 6 print(f"[{nombre}] ¡Respuesta recibida!") 7 return f"Datos de {nombre}" 8 9async def main(): 10 resultado = await consultar_servicio("API_Usuarios", 1.0) 11 print(f"Resultado: {resultado}") 12 13# Ejecutar el bucle de eventos 14asyncio.run(main())
2. Ejecución Concurrente con asyncio.gather() #
asyncio.gather() permite disparar múltiples corrutinas en paralelo y esperar a que todas finalicen:
1import asyncio 2import time 3 4async def descargar_recurso(id_recurso: int) -> str: 5 await asyncio.sleep(0.5) # Simula descarga de 500ms 6 return f"Recurso_{id_recurso}" 7 8async def main(): 9 inicio = time.perf_counter() 10 11 # Ejecuta 3 descargas concurrentemente 12 resultados = await asyncio.gather( 13 descargar_recurso(1), 14 descargar_recurso(2), 15 descargar_recurso(3), 16 ) 17 18 tiempo_total = time.perf_counter() - inicio 19 print(f"Resultados: {resultados}") 20 print(f"Tiempo total: {tiempo_total:.2f} s") # ~0.50 s en lugar de 1.50 s síncronos 21 22asyncio.run(main())Salida de consola Resultados: ['Recurso_1', 'Recurso_2', 'Recurso_3'] Tiempo total: 0.50 s
3. Manejo Estructurado con asyncio.TaskGroup (Python 3.11+) #
TaskGroup es la forma moderna y recomendada para gestionar tareas concurrentes (Structured Concurrency). Si una tarea falla, cancela automáticamente las demás del grupo y propaga las excepciones usando ExceptionGroup:
1import asyncio 2 3async def tarea(id_tarea: int, demora: float): 4 await asyncio.sleep(demora) 5 print(f"Tarea {id_tarea} completada") 6 7async def main(): 8 async with asyncio.TaskGroup() as tg: 9 t1 = tg.create_task(tarea(1, 0.2)) 10 t2 = tg.create_task(tarea(2, 0.1)) 11 t3 = tg.create_task(tarea(3, 0.3)) 12 print("Todas las tareas del grupo han concluido de forma segura") 13 14asyncio.run(main())Salida de consola Tarea 2 completada Tarea 1 completada Tarea 3 completada Todas las tareas del grupo han concluido de forma segura
4. Gestores de Contexto e Iteradores Asíncronos #
Python soporta gestión de contexto e iteración asíncrona mediante async with y async for:
1# Gestor de contexto asíncrono (ejemplo conceptual para conexiones HTTP/BBDD) 2class ConexionAsincrona: 3 async def __aenter__(self): 4 print("Abriendo conexión asíncrona...") 5 await asyncio.sleep(0.1) 6 return self 7 8 async def __aexit__(self, exc_type, exc_val, exc_tb): 9 print("Cerrando conexión asíncrona...") 10 await asyncio.sleep(0.1) 11 12async def demo(): 13 async with ConexionAsincrona(): 14 print("Operando dentro del contexto asíncrono...") 15 16asyncio.run(demo())
Resumen del tema
Conceptos clave #
async def/await: sintaxis nativa para definir y esperar corrutinas sin bloquear el hilo principal.- Bucle de Eventos (Event Loop): orquestador que gestiona la ejecución y el cambio de contexto entre tareas en espera de I/O.
asyncio.gather(*corrutinas): ejecuta y recolecta los resultados de múltiples corrutinas concurrentes.asyncio.TaskGroup(Python 3.11+): estándar moderno de concurrencia estructurada con cancelación segura y captura de errores conExceptionGroup.async with/async for: soporte para protocolos asíncronos en recursos y flujos de datos continuos.
Qué debes recordar #
Usa asyncio para maximizar el rendimiento en operaciones intensivas en I/O (APIs, microservicios, bases de datos) estructurando tus tareas con async with asyncio.TaskGroup().
Laboratorio: esperar, reunir resultados y cancelar #
Compara la espera secuencial con gather mediante un reloj virtual. Provoca un
error o una cancelación y observa al llamador y a cada tarea por separado.
Laboratorio interactivo
Asyncio: tareas, espera y cancelación
Tres operaciones de espera usan un reloj virtual. Compara await secuencial con gather, sin hilos, red ni tiempo de espera real.
Reloj virtual: 0 · Llamador: esperando
En await
Espera restante: 2
Sin resultado
En await
Espera restante: 4
Sin resultado
En await
Espera restante: 1
Sin resultado
Tareas preparadas.
Modelo de esperas cooperativas: omite CPU, I/O y detalles del event loop. La cancelación se observa inmediatamente en un await ficticio. El loop continúa abierto tras el error para ver tareas hermanas; no simula el cierre de asyncio.run ni TaskGroup.
Experimenta: La secuencia normal tarda 7 ticks; gather, 4. Configura B a 1 tick y activa su fallo: el llamador recibe el error mientras A sigue. Con return_exceptions, reúne resultados y errores.