Un Future en Rust representa un valor que todavía no está listo, pero que estará disponible en algún momento. A diferencia de los callbacks o las promesas de otros lenguajes, los futures de Rust son lazy: no hacen nada hasta que alguien los ejecuta.
// Este future no hace nada hasta que alguien lo ejecute:
let f = alguna_funcion_async();
// Solo aquí empieza a ejecutarse:
let resultado = f.await;
El trait Future es la interfaz que define este comportamiento:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
pub enum Poll<T> {
Ready(T),
Pending,
}
El método poll hace una pregunta al future: "¿ya tienes el resultado?" Si lo tiene, devuelve Poll::Ready(valor). Si no, devuelve Poll::Pending.
El Context y el Waker
Cuando un future devuelve Poll::Pending, necesita alguna forma de decirle al runtime cuándo volver a intentarlo. Para eso existe el Waker, que se pasa a través del Context.
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// Si el resultado no está listo...
if !datos_disponibles() {
// Guardar el waker para llamarlo cuando los datos estén listos
let waker = cx.waker().clone();
registrar_callback(move || waker.wake());
return Poll::Pending;
}
Poll::Ready(obtener_datos())
}
El flujo es:
- El runtime llama a
pollcon unContextque contiene unWaker. - Si el future no está listo, guarda el
Wakery devuelvePending. - Cuando el evento que esperaba ocurre (datos de red, timer, etc.), llama a
waker.wake(). - El runtime vuelve a llamar a
pollen el future. - Si ahora sí está listo, devuelve
Ready.
Implementar un Future a mano
Implementemos un future simple que cuenta hasta N de forma async, cediendo el control cada iteración:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct Contador {
valor_actual: u64,
maximo: u64,
}
impl Contador {
pub fn nuevo(maximo: u64) -> Self {
Contador { valor_actual: 0, maximo }
}
}
impl Future for Contador {
type Output = u64;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u64> {
self.valor_actual += 1;
if self.valor_actual >= self.maximo {
Poll::Ready(self.valor_actual)
} else {
// Decirle al runtime que nos llame de nuevo inmediatamente
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
// Uso:
#[tokio::main]
async fn main() {
let resultado = Contador::nuevo(5).await;
println!("Contador llegó a: {}", resultado); // 5
}
Este future llama a waker.wake_by_ref() antes de devolver Pending, lo que hace que el runtime lo vuelva a ejecutar inmediatamente. En un caso real, el waker se llamaría desde un callback de I/O o un timer.
El desugar de async fn
Cuando escribes una función async fn, el compilador la transforma en una función que devuelve una implementación de Future. Esta transformación se llama desugar.
// Lo que escribes:
async fn suma(a: u32, b: u32) -> u32 {
a + b
}
// Lo que el compilador genera (aproximadamente):
fn suma(a: u32, b: u32) -> impl Future<Output = u32> {
struct SumaFuture { a: u32, b: u32, estado: u8 }
impl Future for SumaFuture {
type Output = u32;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<u32> {
// Esta función no tiene puntos .await, así que siempre termina inmediatamente
Poll::Ready(self.a + self.b)
}
}
SumaFuture { a, b, estado: 0 }
}
Para funciones con puntos .await, el compilador genera una máquina de estados donde cada punto .await es una transición de estado:
// Lo que escribes:
async fn secuencial() {
let a = operacion_1().await;
let b = operacion_2(a).await;
println!("{}", b);
}
// Lo que el compilador genera (simplificado):
enum SecuencialState {
Estado0,
Estado1 { future_1: Operacion1Future },
Estado2 { a: Resultado1, future_2: Operacion2Future },
Estado3 { b: Resultado2 },
Terminado,
}
impl Future for SecuencialFuture {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
loop {
match self.estado {
Estado0 => {
self.estado = Estado1 { future_1: operacion_1() };
}
Estado1 { future_1 } => {
match Pin::new(future_1).poll(cx) {
Poll::Ready(a) => {
self.estado = Estado2 { a, future_2: operacion_2(a) };
}
Poll::Pending => return Poll::Pending,
}
}
// ... etc.
}
}
}
}
Esta máquina de estados es lo que hace que los futures sean auto-referenciales (pueden contener referencias a datos en la misma struct) y por qué necesitan Pin.
Por qué los futures son lazy
Un future no hace nada hasta que alguien llama a poll. Esto tiene consecuencias importantes:
// Este código NO hace ninguna petición de red:
let request = reqwest::get("https://ejemplo.com");
// La petición empieza aquí:
let respuesta = request.await?;
Si tienes un future y lo descartas sin ejecutarlo, no ocurre nada. Esto contrasta con los Promises de JavaScript, que empiezan a ejecutarse cuando se crean.
La lazyness permite también la composición: puedes crear futures que envuelven otros futures y controlan exactamente cuándo y cómo se ejecutan:
// timeout envuelve el future y lo cancela si tarda demasiado
let resultado = tokio::time::timeout(
std::time::Duration::from_secs(5),
operacion_lenta()
).await?;
El executor: quién llama a poll
Los futures no se ejecutan solos. Necesitan un executor (o runtime) que los ejecute. En Rust, el executor más usado es Tokio:
// El atributo #[tokio::main] configura el executor
#[tokio::main]
async fn main() {
// Dentro de aquí, puedes usar .await
let resultado = mi_future().await;
}
Tokio mantiene una cola de futures listos para ejecutar, los llama con poll, y cuando devuelven Pending, los pone a dormir hasta que su Waker los despierte. Ese es todo el modelo de ejecución async de Rust.
Entender este modelo ayuda a diagnosticar problemas reales: si un future no progresa, es porque su Waker nunca se llama. Si el sistema va lento, puede ser porque un future que debería devolver Pending está bloqueando el hilo en un loop.
