Asyncio en Python: Cómo ejecutar tareas simultáneas sin bloqueos
¿Alguna vez te ha pasado que mientras Python está esperando algo lento, como leer de la red o del disco, parece que todo se detiene? Con asyncio
, tu código puede seguir avanzando sin quedarse atascado.
Pero antes de entrar en detalles y ver ejemplos prácticos, vamos a repasar algunos conceptos clave.
Entendiendo Asyncio
- Bucle de eventos: El “cerebro” detrás de todo, que coordina las tareas. Cuando una función asíncrona está esperando por algo, el bucle sigue avanzando con otras tareas.
- Corutinas: Son funciones que se definen con
async def
. A diferencia de las funciones normales, pueden detenerse y retomarse más tarde. Ideal para cuando tienes que esperar por operaciones lentas (como descargar una página web). - Await: Es la palabra clave que usamos para decir “pausa aquí mientras esperamos por algo”. Mientras el programa espera, puede seguir con otras tareas.
- Tareas y futuros: Son las tareas ya en ejecución. Un futuro es simplemente una tarea que aún no ha terminado, como ese paquete que pediste online y sigue “en camino”.
Primer paso: await
y async def
Un ejemplo clásico de asyncio
para que te hagas una idea de cómo funciona.
import asyncio
async def di_hola_async():
await asyncio.sleep(2)
print("Hola, mundo asíncrono")
asyncio.run(di_hola_async())
Estamos usando await asyncio.sleep(2)
para simular una espera de 2 segundos. Mientras Python “espera”, podría estar haciendo otras cosas. Así que cuando el tiempo pasa, imprime “Hola, mundo asíncrono”.
Haciendo múltiples tareas a la vez
¿Quieres hacer varias cosas al mismo tiempo sin bloquear tu programa? asyncio.gather
es tu mejor aliado.
import asyncio
async def di_hola_async():
await asyncio.sleep(2)
print("Hola, mundo asíncrono")
async def haz_algo_mas():
print("Empezando otra tarea...")
await asyncio.sleep(1)
print("Terminó la otra tarea")
async def main():
await asyncio.gather(
di_hola_async(),
haz_algo_mas(),
)
asyncio.run(main())
Mientras di_hola_async()
espera, la función haz_algo_mas()
sigue su curso. Así, ambas tareas avanzan en paralelo, optimizando el uso del tiempo.
Caso práctico: Peticiones a páginas web con asyncio
Aquí es donde asyncio
brilla de verdad. Las tareas de entrada/salida, como realizar peticiones a páginas web, suelen ser lentas. Si las hacemos secuencialmente, perderíamos un montón de tiempo esperando. Vamos a ver cómo hacerlo de manera eficiente usando aiohttp
para manejar múltiples descargas simultáneamente.
El enfoque clásico (bloqueante):
import requests
import time
def peticion(url):
return requests.get(url).text
start_time = time.time()
pagina1 = peticion('http://example.com')
pagina2 = peticion('http://example.org')
print(f"Todo listo en {time.time() - start_time} segundos")
Y ahora el enfoque asíncrono con asyncio
:
import aiohttp
import asyncio
import time
async def peticion_async(url, session):
async with session.get(url) as respuesta:
return await respuesta.text()
async def main():
async with aiohttp.ClientSession() as session:
pagina1 = asyncio.create_task(peticion_async('http://example.com', session))
pagina2 = asyncio.create_task(peticion_async('http://example.org', session))
await asyncio.gather(pagina1, pagina2)
start_time = time.time()
asyncio.run(main())
print(f"Todo listo en {time.time() - start_time} segundos")
En este caso, ambas peticiones ocurren simultáneamente, lo que reduce el tiempo total de espera. Esto es ideal cuando tienes que hacer muchas peticiones a la vez, como en web scraping o aplicaciones que manejan APIs.
¿Y si necesitas mezclar código síncrono y asíncrono?
A veces, tenemos partes del código que no pueden ser asíncronas, como una función que hace cálculos pesados o una API que solo tiene métodos síncronos. En ese caso, no te preocupes, hay una forma de combinar ambas cosas.
import asyncio
import time
def tarea_sincrona():
print("Empezando una tarea síncrona...")
time.sleep(5)
print("Tarea síncrona terminada")
async def async_wrapper():
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, tarea_sincrona)
async def main():
await asyncio.gather(
async_wrapper(),
# Aquí puedes agregar más tareas asíncronas
)
asyncio.run(main())
Aquí usamos loop.run_in_executor()
para ejecutar la tarea síncrona en un hilo separado, sin bloquear el bucle de eventos de asyncio. Así puedes seguir usando código síncrono sin perder la eficiencia que te da asyncio
.
Otros usos prácticos: Leer archivos con asyncio
Una tarea común es leer varios archivos de forma eficiente, especialmente si son grandes o tienes muchos. Para eso podemos usar aiofiles
y aprovechar la programación asíncrona también en la lectura de archivos.
import asyncio
import aiofiles
async def lee_archivo_async(ruta):
async with aiofiles.open(ruta, 'r') as archivo:
return await archivo.read()
async def lee_todos_async(rutas):
tareas = [lee_archivo_async(ruta) para ruta en rutas]
return await asyncio.gather(*tareas)
async def main():
rutas = ['archivo1.txt', 'archivo2.txt']
datos = await lee_todos_async(rutas)
print(datos)
asyncio.run(main())
Con aiofiles
podemos leer varios archivos simultáneamente sin bloquear el programa, haciéndolo mucho más eficiente cuando tienes muchas operaciones de I/O.
Conclusión: Asyncio es más sencillo de lo que parece
Al principio, puede que asyncio
parezca complicado con sus bucles de eventos, corutinas y awaits. Pero una vez que lo pruebes con casos reales, descubrirás lo útil que es para escribir código eficiente y moderno. Ya sea manejando redes, archivos o cualquier operación de I/O, asyncio
te permitirá aprovechar al máximo el tiempo de espera y hacer que tu código sea mucho más rápido.
¡Anímate a usar asyncio
en tus proyectos! Cuando veas lo que puedes hacer con esto, no querrás volver atrás.