Toda la diferencia está en qué hace el hilo mientras espera una respuesta de la red, de la base de datos o del disco: quedarse detenido, o atender otra cosa mientras tanto.
Avanza con → ← o con los botones de abajo.
El mesero es el hilo. La cocina es la base de datos o la API externa. Cada mesa es una petición que llega a tu servidor.
Toma el pedido de la mesa 1, lo lleva a la cocina y se queda ahí hasta que el plato esté listo. Las mesas 2 y 3 esperan aunque él no esté haciendo nada.
El tiempo total es la suma de todas las esperas.
Cada mesero sigue esperando parado en la cocina, pero como hay tres, se atienden tres mesas a la vez.
Funciona, pero cada mesero cuesta (memoria) y se pueden chocar si tocan lo mismo (datos compartidos).
Deja el pedido en la cocina, va a la mesa 2, luego a la 3, y cuando la cocina toca la campana vuelve a buscar el plato de la mesa 1.
Un solo mesero atiende muchas mesas porque aprovecha las esperas.
import requests
def obtener_usuario(id):
r = requests.get(f"https://api.ejemplo.com/users/{id}") # espera aquí
return r.json()
def main():
for id in [1, 2, 3]:
print(obtener_usuario(id))
requests.get espera la respuesta, el hilo queda bloqueado: la CPU casi no trabaja, pero el hilo no puede atender nada más.from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=3) as pool:
resultados = list(pool.map(obtener_usuario, [1, 2, 3]))
threading.Lock para evitar condiciones de carrera.import asyncio
import httpx
async def obtener_usuario(cliente, id):
r = await cliente.get(f"https://api.ejemplo.com/users/{id}") # cede el hilo
return r.json()
async def main():
async with httpx.AsyncClient() as cliente:
resultados = await asyncio.gather(
*(obtener_usuario(cliente, id) for id in [1, 2, 3])
)
asyncio.run(main())
async def define una corrutina. Llamarla no la ejecuta: devuelve un objeto que el event loop puede pausar y reanudar.await es el punto donde la función se pausa y le devuelve el hilo al event loop. Mientras la red responde, el loop ejecuta otras corrutinas que están listas.await, nunca en medio de una línea.El loop mantiene una cola de listos (tareas que pueden avanzar ya) y una lista de esperando (tareas pausadas en un await). Siempre ejecuta la siguiente lista; nunca la que espera.
Cada petición llega 100 ms después de la anterior, procesa 100 ms, espera la base de datos y procesa 100 ms más para responder. Cambia el modelo y mira qué hace el hilo en cada momento.
time.sleep dentro de asyncasync def tarea():
time.sleep(2) # congela TODO el loop
await asyncio.sleep(2) # pausa solo esta tarea
Una llamada bloqueante dentro de async def no cede el hilo. Todas las demás peticiones quedan detenidas.
async def leer():
r = requests.get(URL) # bloquea
r = await cliente.get(URL) # httpx.AsyncClient
r = await asyncio.to_thread(requests.get, URL)
requests, psycopg2 o pymysql bloquean. Usa sus versiones async (httpx, asyncpg, aiomysql) o mándalas a un hilo con asyncio.to_thread.
awaitdatos = obtener_usuario(cliente, 1) # coroutine, no datos datos = await obtener_usuario(cliente, 1) # ahora sí
Sin await la corrutina nunca se ejecuta y Python avisa con RuntimeWarning: coroutine ... was never awaited.
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
pool.map(redimensionar_imagen, archivos)
Async aprovecha esperas. Si la función usa CPU todo el rato no hay espera que aprovechar. Para cálculo pesado usa procesos, que sí corren en varios núcleos.
def o async def@app.get("/reporte")
def reporte():
r = requests.get(URL) # FastAPI lo ejecuta en un pool de hilos
return r.json()
@app.get("/usuarios")
async def usuarios():
async with httpx.AsyncClient() as c:
r = await c.get(URL) # corre directo en el event loop
return r.json()
@app.get("/peligro")
async def peligro():
r = requests.get(URL) # bloquea a TODOS los clientes
return r.json()
def, FastAPI manda la función a un pool de hilos (por defecto, hasta 40 a la vez vía AnyIO). Es el modelo de "más meseros".async def, la función corre en el event loop. Escala a muchas peticiones simultáneas con un solo hilo, siempre que todo lo que espera use await./peligro es el peor de los dos mundos: está en el loop, pero bloquea. Mientras espera, ninguna otra petición avanza.async def. Si usas librerías bloqueantes (un ORM síncrono, requests), deja def normal.| Tu situación | Herramienta | Por qué |
|---|---|---|
| Muchas peticiones que pasan la mayor parte del tiempo esperando red o BD: APIs, scraping, websockets, bots | asyncio | Miles de esperas simultáneas en un solo hilo, con poca memoria. |
| Hay que esperar I/O, pero la librería solo existe en versión síncrona | ThreadPoolExecutor asyncio.to_thread | El hilo se bloquea, pero el GIL se libera durante la espera y otro hilo avanza. |
| Cálculo intensivo: procesar imágenes, modelos, compresión, números | ProcessPoolExecutor multiprocessing | Cada proceso tiene su propio intérprete y su propio GIL, así que usa varios núcleos. |
| Script simple que hace una cosa después de otra | síncrono | Más fácil de leer, de depurar y de testear. No agregues complejidad si no hay esperas que solapar. |
Una función síncrona bloquea el hilo mientras espera. Una asíncrona se pausa y presta el hilo a otra tarea hasta que llegue su resultado.
await.async def.Ejercicio sugerido: escribe un script que pida 10 URLs con requests en un for, luego con ThreadPoolExecutor y luego con httpx.AsyncClient + asyncio.gather. Mide cada versión con time.perf_counter().