Aprende leyendo en orden

Fundamentos de async / await — Acelera tus programas aprovechando el tiempo de espera

Aprende corrutinas, bucle de eventos y asyncio.sleep, y cómo async def / await delegan la espera de I/O a otra tarea para acelerar la ejecución.

async / await es un mecanismo para hacer otro trabajo mientras esperas operaciones de E/S (Entrada/Salida — lectura de archivos, llamadas de red, consultas a la BD y otras operaciones dominadas por el tiempo de espera). Puede acelerar tareas como llamar 100 veces a una Web API, sin salir de un solo hilo. Este artículo recorre tres ideas centrales: las corrutinas, el bucle de eventos y asyncio.sleep.

Sobre la ejecución de código aquí

async / await es todo cuestión de tiempos, pero el runner de este sitio acumula la salida de print y la muestra toda de golpe cuando termina el script. La salida en tiempo real y la sensación del tiempo transcurrido no coincidirán con un entorno Python real. El artículo usa diagramas para dejar claro el comportamiento interno, pero si quieres ver el flujo de print en vivo o sentir los tiempos reales, ejecútalo en tu entorno Python local con asyncio.run.

Dónde brilla async / await
Llamar a 100 Web APIsen concurrenciaEjecutar consultas BDen concurrenciaProcesar E/S de archivosen rutas paralelasWeb scrapingde muchas páginas a la vez
Ideal para trabajo dominado por la espera. async cambia a otra tarea mientras la CPU está ociosa (esperando E/S / red / sleep), manteniendo el programa en un solo hilo y haciendo que todo termine antes.

Proceso e hilo — el contexto de async / await

Antes de meternos con async / await, fijemos los conceptos de proceso e hilo.

Un proceso (un programa en ejecución desde el punto de vista del SO) es una unidad de ejecución independiente con su propio espacio de memoria. Ejecutas un script Python y obtienes un proceso; ejecutas otro en otra terminal y tienes dos.

Un hilo (el flujo que realmente ejecuta el código dentro de un proceso) es la unidad que usa la CPU para hacer avanzar el código. Un proceso puede tener varios hilos, pero un programa Python típico corre como 1 proceso + 1 hilo, y una espera como time.sleep detiene ese hilo.

Proceso / Hilo / Corrutina — capas anidadas
Proceso (una app Python)
  • Una unidad de ejecución independiente desde la vista del SO
  • Tiene su propio espacio de memoria
Hilo (un flujo de ejecución)
  • La unidad que realmente ejecuta el código dentro de un proceso
  • Python normal suele tener solo uno corriendo
Corrutina (una función async def)
  • Se turna dentro de un mismo hilo
  • Puedes crear cuantas quieras con async / await
Tres niveles de anidamiento: un proceso contiene un hilo, que contiene corrutinas. async / await solo planifica las corrutinas más internas — nunca añade hilos ni CPUs. Esa es la diferencia clave con threading (añade hilos) y multiprocessing (añade procesos).

async / await solo gestiona las corrutinas más internas. Cambia entre corrutinas en el mismo hilo, sin añadir hilos ni CPUs — solo cede el control a otra corrutina mientras la CPU está ociosa esperando E/S. Para ejecutar hilos verdaderamente en paralelo usa threading; para añadir procesos usa multiprocessing (los siguientes dos artículos los cubren).

Qué es una corrutina — la estrella de async / await

Una corrutina (una función / objeto definida con async def que puede pausarse en puntos await internos) está en el corazón de async / await. Una función normal corre de principio a fin cuando la llamas, pero una corrutina puede pausarse en cada await interno y reanudarse cuando el bucle de eventos se lo indica. Esa es la base sobre la que se construye async.

Llamar a una función async def no ejecuta su cuerpo — solo devuelve un objeto corrutina (por ejemplo, coro = hello()). El cuerpo solo empieza a ejecutarse cuando haces await coro o se lo pasas a asyncio.run(coro).

import asyncio

async def hello():
    return "Hi"

# Llamarla no ejecuta el cuerpo
coro = hello()
print(coro)             # <coroutine object hello at 0x...>
                        # ↑ 0x... es una dirección de memoria.
                        #   Solo significa "se creó una corrutina"

# await la ejecuta de verdad
print(await coro)       # Hi
Estados de una corrutina
async def f()definidaf() llamadacorrutina creadaawait f()→ empieza a correrreturn→ terminada
Llamar a una función async def no ejecuta el cuerpo — solo devuelve un objeto corrutina. await la pone realmente en marcha, y return la termina. Los await internos pausan y reanudan en el medio.

Cuando escribes await some_io(), la corrutina se pausa y cambia a otra tarea, y otras corrutinas pueden ejecutarse mientras espera — esa es la característica que define a async.

Qué es el bucle de eventos — el planificador que cambia entre corrutinas

El bucle de eventos (un planificador que cambia entre corrutinas por turnos) es lo que asyncio ejecuta por debajo. Repite sin fin: toma una corrutina de la cola → la ejecuta → cambia a otra en await → vuelve a poner en la cola las esperas completadas.

import asyncio

async def main():
    print("start")
    await asyncio.sleep(0)   # sleep(0) en realidad no espera,
                             #   solo marca "aquí se puede cambiar"
    print("end")

# asyncio.run inicia el bucle → corre main → cierra el bucle al terminar
asyncio.run(main())
# Salida:
# start
# end
Cómo funciona el bucle de eventos
Tomar dela colaEjecutar lacorrutinaEn await,cambiar fueraE/S lista →de vuelta a la cola
El bucle de eventos toma una corrutina lista de la cola y la ejecuta, cambia a otra cuando una llega a await y vuelve a colocar la corrutina en la cola cuando su E/S termina — repitiendo eternamente en un solo hilo.

Todo esto pasa en un solo hilo — sin núcleos de CPU adicionales, solo rellenando el tiempo de espera ocioso con otra corrutina. En el Python del navegador de este sitio, el bucle de eventos ya está corriendo, así que puedes escribir await directamente en el nivel superior sin llamar a asyncio.run(...).

Por qué async / await — cambiar a otra tarea durante las esperas

En el código síncrono (sync) (el estilo normal línea por línea), time.sleep(1) bloquea el programa — la CPU está ociosa, pero el programa está congelado. Lo mismo pasa con las respuestas de Web APIs, las consultas a la BD y la finalización de E/S de archivos.

async / await te permite cambiar a otra tarea allí donde escribas "esperar", manteniéndote en el mismo hilo pero cambiando en el momento en que arranca una espera.

# requests = cliente HTTP síncrono (una llamada a la vez)
# httpx    = cliente HTTP con soporte async (await en llamadas paralelas)
import requests, asyncio, httpx

# Sync: pega 3 APIs una por una → 3 segundos en total
def fetch_users_sync():
    r1 = requests.get("https://api.example.com/users/1")  # ← espera 1 seg
    r2 = requests.get("https://api.example.com/users/2")  # ← otro 1 seg
    r3 = requests.get("https://api.example.com/users/3")  # ← otro 1 seg
    return [r1.json(), r2.json(), r3.json()]

# Async: pega las 3 en paralelo → termina en ~1 seg (la más lenta)
async def fetch_users_async():
    async with httpx.AsyncClient() as client:
        r1, r2, r3 = await asyncio.gather(
            client.get("https://api.example.com/users/1"),
            client.get("https://api.example.com/users/2"),
            client.get("https://api.example.com/users/3"),
        )
        return [r1.json(), r2.json(), r3.json()]

Los detalles de asyncio.gather vienen después

El asyncio.gather(...) que se usa aquí ejecuta varias corrutinas en concurrencia y espera todos los resultados. La semántica detallada — valores de retorno, manejo de excepciones — se cubre en el siguiente artículo: Tasks de asyncio.

async — varias tareas turnándose
A corriendoB esperandoC esperandoA: await→ cambia fueraB corriendoC esperandoA esperandoB: await→ cambia fueraC corriendocambiocambio
Tres corrutinas comparten una CPU en el mismo hilo. Cada tarea cambia fuera en await cuando empieza a esperar, y la siguiente corre. El tiempo de espera avanza en paralelo sin abandonar nunca el hilo único — ese es el corazón de async.
Concurrente vs Paralelo
ConcurrenteAlternar entretareas en una CPUasync / await(este artículo)ParaleloVarios núcleos de CPUcorren a la vezthreading /multiprocessing(siguientes artículos)
Concurrente = alternar entre tareas en una CPU (lo que te da async / await). Paralelo = varios núcleos de CPU corriendo a la vez de verdad (logrado con threading / multiprocessing). async no acelera código que usa el 100 % de la CPU.

Concurrente, no paralelo

async / await nunca añade CPU — el código que satura la CPU (cómputo pesado) no se acelerará. Solo rellena el tiempo de CPU ocioso durante "espera de E/S / espera de red / sleep" cambiando a otra tarea. Esto es ejecución concurrente, no ejecución paralela real. Para correr de verdad en varios núcleos necesitas threading o multiprocessing — cubiertos en los siguientes dos artículos.

async def y await — lo básico

Una función definida con async def se llama función corrutinallamarla no ejecuta el cuerpo, solo devuelve un objeto corrutina. Para ejecutarla de verdad, hazle await o pásasela a asyncio.run().

await x significa "espera a que x termine, cambiando a otra tarea mientras tanto". x puede ser una de tres cosas: una corrutina (en lo que se centra este artículo), una Task o un Future (el objeto Task que vemos en el siguiente artículo, más el objeto de notificación de finalización que usa internamente) — en el código del día a día usarás sobre todo corrutinas o Tasks.

import asyncio

# Define una función corrutina con async def
async def hello():
    return "Hi"

# Llamarla solo devuelve un objeto corrutina (el cuerpo no se ejecuta)
print(hello())                    # <coroutine object hello at 0x...>

# await la ejecuta (el await de nivel superior funciona en este entorno del navegador)
result = await hello()
print(result)                     # Hi

# En un script Python real, envuélvelo con asyncio.run()
# print(asyncio.run(hello()))     # Hi
async def y await
async def hello(): return 'Hi'resultado de hello()= una corrutina(cuerpo no ejecutado)await hello()→ devuelve 'Hi'solo llamadaawait
Define con async defllamar por sí solo devuelve un objeto corrutina con el cuerpo sin ejecutar → await la ejecuta de verdad y entrega el resultado. Esa es la regla mínima de async / await.
ElementoSignificadoNotas
async def f():Define una función corrutinaLlamarla no ejecuta el cuerpo
f()Crea un objeto corrutinaNecesita await para correr de verdad
await f()Espera la finalización, cambiando a otrasSolo legal dentro de una función async
asyncio.sleep(N)Espera N segundos (cediendo durante la espera)No bloquea como time.sleep
asyncio.run(f())Ejecuta desde el nivel superiorPunto de entrada estándar en Python real

Sin await, la corrutina nunca corre

Si escribes hello() solo, el cuerpo no se ejecuta nunca y verás un aviso como <coroutine object hello at 0x...> en la consola. Haz siempre await hello() o ejecútala vía asyncio.run(hello()). "Llamar" y "ejecutar" son dos cosas distintas en async — no las confundas.

Escribe una pequeña función async que simule una llamada a una API y luego ejecútala con await. Usaremos asyncio.sleep(0.5) para simular un tiempo de respuesta de 0.5 segundos.

① Añade import asyncio.

② Define async def fetch_user(user_id): — imprime f"user {user_id} start", haz await asyncio.sleep(0.5) para esperar 0.5 seg, imprime f"user {user_id} done", y luego return f"User{user_id}".

③ Ejecuta con result = await fetch_user(1) y guarda el valor (el await de nivel superior funciona aquí).

④ Imprime f"result: {result}".

(Ejecuta con éxito y aparecerá la explicación.)

Editor Python

Ejecutar el código para ver el resultado

La ejecución concurrente comparte la espera — la recompensa de asyncio

asyncio.sleep(seconds) es la versión async de sleepcambia a otra tarea durante la espera. Combinada con asyncio.gather (siguiente artículo) para correr varias corrutinas a la vez, todos los sleeps avanzan en concurrencia, así que el tiempo total se vuelve el sleep individual más largo (no la suma, como te daría time.sleep).

Internamente, asyncio.sleep(N) registra "reanudar esta corrutina en N segundos" en el bucle y cambia fuera de inmediato. En cambio, time.sleep(N) es una llamada al SO que bloquea por completo la CPU — el bucle también se detiene y ninguna otra corrutina puede correr. Usar time.sleep dentro de una función async tira por la borda toda la idea de async, así que cuidado.

Por dentro de asyncio.sleep
awaitasyncio.sleep(1)Avisa al bucle:"despiértame en 1 s"Mientras tanto otrascorrutinas corren1 seg después→ reanudar
asyncio.sleep(N) registra "reanudar en N segundos" en el bucle de eventos y cambia fuera. Durante esa ventana, otras corrutinas corren. Tras N segundos la corrutina vuelve a la cola y se reanuda.
time.sleep vs asyncio.sleep
time.sleep(1)(sync)Bloquea la CPUdurante 1 segundoOtras tareas asynctambién se congelanasyncio.sleep(1)(async)Cede al bucledurante 1 segundoOtras tareaspueden avanzar
time.sleep bloquea totalmente la CPU, así que otras tareas async no pueden correr. asyncio.sleep cambia a otra tarea, dejándolas avanzar — usa siempre asyncio.sleep dentro de funciones async.

Llama al mismo fetch_user 3 veces en paralelo y observa cómo un trabajo secuencial de 1.5 segundos termina en 0.5 segundos. Usamos asyncio.gather para la ejecución en paralelo (todos los detalles en el siguiente artículo).

① Añade import asyncio.

② Define async def fetch_user(user_id): — igual que en el ejercicio anterior (print de start → await asyncio.sleep(0.5) → print de done → return).

③ Ejecuta con results = await asyncio.gather(fetch_user(1), fetch_user(2), fetch_user(3)) para disparar 3 llamadas en paralelo.

④ Imprime la lista de resultados como f"results: {results}".

Editor Python

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Dada async def hello(): return 'Hi', ¿qué devuelve hello()?

Pregunta 2Dentro de una función async, ¿cuál "espera N segundos cambiando a otra tarea"?

Pregunta 3¿Qué carga de trabajo se beneficia más de async / await?