
Futures, Awaitables y Errores Comunes en asyncio
Tabla de Contenido
En los artículos anteriores aprendimos a crear coroutines con async/await
y ejecutar tasks concurrentes. En este artículo vamos a cerrar el modelo mental: entenderemos qué son los futures, cómo se relacionan con tasks y coroutines a través de los awaitables, y veremos cómo medir el rendimiento de nuestro código asíncrono. Para cerrar, hablaremos de los dos errores más comunes al trabajar con asyncio.
Futures: Un Valor que Aún No Existe
Un future es un objeto de Python que representa un valor que esperamos obtener en algún momento, pero que todavía no tenemos. Cuando se crea, un future está en estado incompleto. Una vez que el valor llega, se establece con set_result y el future pasa a estar completado.
from asyncio import Future
my_future = Future()
print(f'¿Está completo? {my_future.done()}') # False
my_future.set_result(42)
print(f'¿Está completo? {my_future.done()}') # True
print(f'Resultado: {my_future.result()}') # 42
Futures en la práctica
En la mayoría de los casos no crearás futures directamente — asyncio los gestiona internamente. Pero entender su mecánica es clave para comprender cómo funciona todo por dentro. Veamos un ejemplo donde un task establece el valor de un future después de un segundo:
from asyncio import Future
import asyncio
def hacer_peticion() -> Future:
future = Future()
asyncio.create_task(establecer_valor(future)) # 1
return future
async def establecer_valor(future) -> None:
await asyncio.sleep(1) # 2
future.set_result(42)
async def main():
future = hacer_peticion()
print(f'¿Está completo? {future.done()}') # False
valor = await future # 3
print(f'¿Está completo? {future.done()}') # True
print(valor) # 42
asyncio.run(main())
- Crea un task que configurará el valor del future.
- Espera 1 segundo antes de establecer el resultado.
awaitpausamain()hasta que el future reciba su valor.
La Jerarquía de los Awaitables
Tanto las coroutines como los tasks pueden usarse con await. ¿Qué tienen en común? Ambos son awaitables — objetos que implementan el método __await__, que es lo que Python necesita para poder usar la palabra clave await sobre ellos.
La relación entre estos conceptos forma una jerarquía clara:
- Awaitable es la clase base que define el protocolo
__await__. Todo lo que se pueda usar conawaites un awaitable. - Coroutine es una función definida con
async def. Hereda deAwaitable. - Future representa un valor pendiente. También hereda de
Awaitable. - Task hereda de
Future. Es la combinación de un future y una coroutine: encapsula una coroutine y la ejecuta en el event loop, mientras que el future subyacente almacena el resultado cuando la coroutine termina.
Note
Un Future es qué esperamos obtener. Un Task es cómo lo obtenemos (ejecutando una coroutine). Y Awaitable es el contrato que ambos cumplen para que await funcione.
Midiendo el Tiempo de Ejecución
Cuando trabajamos con código asíncrono, medir cuánto tarda cada operación es esencial para verificar que la concurrencia realmente funciona. La mejor forma de hacerlo es con un decorador que registre el tiempo de inicio y fin de cada coroutine:
import functools
import time
from typing import Callable, Any
def async_timed():
def wrapper(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapped(*args, **kwargs) -> Any:
print(f'starting {func} with args {args} {kwargs}')
start = time.time()
try:
return await func(*args, **kwargs)
finally:
end = time.time()
total = end - start
print(f'finished {func} in {total:.4f} second(s)')
return wrapped
return wrapper
Uso en la práctica
import asyncio
@async_timed()
async def delay(delay_seconds: int) -> int:
print(f'sleeping for {delay_seconds} second(s)')
await asyncio.sleep(delay_seconds)
print(f'finished sleeping for {delay_seconds} second(s)')
return delay_seconds
@async_timed()
async def main():
task_one = asyncio.create_task(delay(2))
task_two = asyncio.create_task(delay(3))
await task_one
await task_two
asyncio.run(main())
starting <function main ...> with args () {}
starting <function delay ...> with args (2,) {}
starting <function delay ...> with args (3,) {}
finished <function delay ...> in 2.0032 second(s)
finished <function delay ...> in 3.0003 second(s)
finished <function main ...> in 3.0004 second(s)
El output confirma lo que esperamos: ambos tasks corren concurrentemente. delay(2) termina a los ~2 segundos, delay(3) a los ~3, y main() tarda ~3 segundos en total (no 5).
Errores Comunes en asyncio
Hay dos trampas frecuentes que pueden arruinar el rendimiento de una aplicación async. Ambas tienen la misma causa: bloquear el event loop.
1. Ejecutar código CPU-bound en coroutines
Cuando tenemos funciones computacionalmente costosas (cálculos matemáticos, procesamiento de datos grandes), puede parecer buena idea envolverlas en tasks para ejecutarlas concurrentemente. Pero recuerda: asyncio usa un solo hilo. El GIL garantiza que solo una instrucción de bytecode se ejecute a la vez, así que el código CPU-bound bloqueará el event loop sin importar cuántos tasks crees.
2. Usar APIs bloqueantes
El mismo problema ocurre al usar librerías de I/O que no son asíncronas. Librerías como requests o funciones como time.sleep bloquean el hilo principal. Cuando las llamas dentro de una coroutine, bloqueas el event loop completo y ninguna otra coroutine puede ejecutarse.
- ❌ Bloqueante
- ✅ No bloqueante
No uses estas funciones dentro de coroutines
requests.get()→ bloquea el event looptime.sleep()→ bloquea el event loop- Cualquier función de I/O síncrona
Warning
Regla general: cualquier función que realice I/O y no sea una coroutine, o que ejecute operaciones CPU costosas, debe considerarse bloqueante. Para CPU-bound, usa loop.run_in_executor()
con un ProcessPoolExecutor.
En conclusión
- Un Future representa un valor que aún no existe — se completa con
set_result. - Un Task es un future + una coroutine: ejecuta la coroutine y almacena su resultado en el future subyacente.
- Todo lo que se pueda usar con
awaites un Awaitable — el contrato que unifica coroutines, futures y tasks. - El decorador
@async_timedpermite verificar que la concurrencia funciona midiendo tiempos reales. - Los dos errores más comunes son ejecutar código CPU-bound y usar APIs bloqueantes dentro de coroutines — ambos bloquean el event loop.


