
asyncio: Los Fundamentos de la Programación Asíncrona en Python
Tabla de Contenido
Si alguna vez construiste un scraper que recorre decenas de páginas, un microservicio que atiende cientos de peticiones o un bot que consume múltiples APIs, seguro sentiste la frustración de esperar. Una petición termina, la siguiente arranca, y tu programa queda ahí sin hacer nada útil.
La librería asyncio de Python fue diseñada para resolver exactamente este problema. En este artículo vamos a construir un modelo mental sólido sobre los conceptos clave: concurrencia, paralelismo, multitarea y la diferencia entre operaciones limitadas por CPU y por E/S.
¿Qué es la programación asíncrona?
La programación asíncrona es un modelo donde las tareas de larga duración se ejecutan en segundo plano, separadas del flujo principal de tu aplicación. En lugar de bloquear todo mientras esperas una respuesta de red o una lectura de archivo, el sistema queda libre para hacer otro trabajo. Cuando la tarea termina, tu código recibe una notificación y puede procesar el resultado.
Note
asyncio — abreviación de asynchronous I/O — es la librería estándar de Python que implementa este modelo. Fue introducida en Python 3.4
y hoy es la base de frameworks modernos como FastAPI
, aiohttp
y Starlette
.
CPU-bound vs. I/O-bound
Antes de avanzar, necesitas entender una distinción fundamental: ¿en qué está gastando tiempo tu código?
Operaciones limitadas por E/S (I/O-bound)
Tu programa pasa la mayor parte del tiempo esperando un sistema externo: la red, el disco, una base de datos. La CPU está prácticamente ociosa. Ejemplos:
- Hacer peticiones HTTP.
- Leer o escribir archivos en disco.
- Consultar una base de datos.
Operaciones limitadas por CPU (CPU-bound)
El procesador es el cuello de botella. La CPU está activamente calculando, sin esperas externas. Ejemplos:
- Calcular dígitos de pi.
- Aplicar lógica de negocio sobre estructuras de datos grandes.
- Procesamiento de imágenes o entrenamiento de modelos.
Ejemplo práctico
Considera este fragmento de código:
import requests
response = requests.get('https://www.example.com') # 1 - I/O-bound
items = response.headers.items()
headers = [f'{key}: {header}' for key, header in items] # 2 - CPU-bound
formatted_headers = '\n'.join(headers) # 3 - CPU-bound
with open('headers.txt', 'w') as file:
file.write(formatted_headers) # 4 - I/O-bound
El siguiente diagrama muestra el flujo de ejecución y clasifica cada paso:
🔵 Azul = I/O-bound (esperando red o disco) · 🟡 Amarillo = CPU-bound (procesando en memoria)
Info
asyncio brilla con cargas I/O-bound. Si tu cuello de botella es la CPU, necesitarás multiprocessing o concurrent.futures
.
Concurrencia vs. Paralelismo
Estos dos términos se confunden constantemente, pero describen cosas distintas.
Concurrencia: alternar entre tareas
Concurrencia significa gestionar múltiples tareas en períodos de tiempo superpuestos, sin necesidad de ejecutarlas en el mismo instante. Piensa en un chef preparando dos platos: pica verduras para el plato A, lo mete al horno y empieza a trabajar en el plato B mientras el A se cocina. Solo hace una cosa a la vez, pero ambos platos avanzan.
En un sistema concurrente, una sola CPU alterna entre tareas. Mientras la tarea #1 espera por E/S, la CPU toma la tarea #2.
Paralelismo: ejecución simultánea real
Paralelismo significa ejecutar múltiples tareas al mismo tiempo, lo cual requiere múltiples CPUs o núcleos.
En palabras de Rob Pike (co-creador de Go): “La concurrencia se trata de lidiar con muchas cosas a la vez. El paralelismo se trata de hacer muchas cosas a la vez.”
Note
La concurrencia es un patrón de diseño; el paralelismo es un modelo de ejecución. asyncio logra concurrencia en un solo hilo, sin paralelismo.
Tipos de Multitarea
Existen dos enfoques principales para la multitarea, y entender la diferencia es clave para comprender dónde encaja asyncio.
Multitarea preventiva (Preemptive)
El sistema operativo decide cuándo cambiar entre tareas mediante un mecanismo llamado time slicing: divide el tiempo de CPU en intervalos pequeños y rota entre las tareas activas. Cuando el SO fuerza un cambio, se dice que está haciendo preemption.
En Python, este es el modelo que usan los módulos threading (hilos) y multiprocessing (procesos). La ventaja es que no tienes que pensar en cuándo ceder el control. La desventaja: los cambios pueden ocurrir en cualquier momento, lo que introduce condiciones de carrera y la necesidad de locks.
Multitarea cooperativa (Cooperative)
Las propias tareas deciden cuándo ceder el control. Cada tarea señala explícitamente: “Me pauso aquí, ejecuta otras tareas.” No hay intervención del SO.
Este es exactamente el modelo de asyncio. En Python, la palabra clave await es esa señal explícita:
import asyncio
async def obtener_datos():
print("Obteniendo datos...")
await asyncio.sleep(2) # 👈 "Me pauso aquí, ejecuta otras tareas"
print("Datos obtenidos!")
async def procesar_datos():
print("Procesando datos...")
await asyncio.sleep(1) # 👈 Misma señal
print("Datos procesados!")
async def main():
await asyncio.gather(obtener_datos(), procesar_datos())
asyncio.run(main())
La ventaja es la predictibilidad: tú controlas exactamente dónde ocurren los cambios. El riesgo: si una tarea olvida hacer await o ejecuta una operación CPU-bound larga sin ceder control, bloquea todo el event loop.
El siguiente diagrama de secuencia ilustra cómo el event loop coordina las tareas cooperativamente:
Comparación rápida
- Preventiva
- Cooperativa
Multitarea Preventiva
- ¿Quién cambia? El Sistema Operativo
- Mecanismo: Time slicing
- Herramientas en Python:
threading,multiprocessing - Riesgo principal: Condiciones de carrera
- Ideal para: Cargas CPU-bound o mixtas
Multitarea Cooperativa
- ¿Quién cambia? La propia tarea
- Mecanismo:
await/ yield - Herramientas en Python:
asyncio - Riesgo principal: Bloquear el event loop
- Ideal para: Cargas I/O-bound
¿Dónde encaja asyncio?
Con todos estos conceptos claros, el siguiente diagrama te ayuda a decidir qué herramienta usar según tu caso:
En conclusión
asyncioofrece concurrencia, no paralelismo — todo corre en un solo hilo.- Usa multitarea cooperativa — tu código cede control explícitamente con
await. - Destaca en cargas I/O-bound — peticiones de red, operaciones de archivo, consultas a bases de datos.
- No es la solución para trabajo CPU-bound — para eso, combínalo con
multiprocessingo usa loop.run_in_executor() .


