El modelo de asincronía en Rust es fundamentalmente distinto al de otros lenguajes como JavaScript o Python. Mientras que en esos lenguajes las tareas se gestionan mediante un “push model” (callbacks que se ejecutan cuando el evento ocurre), Rust utiliza un modelo de “pull model” o de sondeo (polling). En este modelo, el motor de ejecución (executor) es el responsable de preguntar activamente a la tarea si está lista para progresar.

El núcleo de este mecanismo es el trait Future. Un Future no es una tarea que se ejecuta en segundo plano por sí misma, sino una máquina de estados que representa un valor que estará disponible en el futuro. Para que este sistema sea eficiente y no consuma CPU innecesariamente en un bucle infinito (busy-waiting), Rust implementa un sistema de notificación basado en Waker.

El modelo de Polling y el contrato del Waker

El método central de un Future es poll. A diferencia de una promesa que se resuelve mediante una notificación externa, un Future se “empuja” hacia adelante mediante llamadas repetitivas a poll.

Cuando se llama a poll, el método devuelve un enum Poll<Self::Output> con dos variantes:
1. Poll::Ready(val): La tarea ha finalizado y el valor está listo.
2. Poll::Pending: La tarea no puede continuar (por ejemplo, está esperando I/O) y debe ceder el control al executor.

Aquí reside la clave de la eficiencia: si poll devuelve Pending, el Future tiene la obligación contractual de asegurar que, en algún momento, el Waker (contenido dentro del Context) sea invocado mediante su método wake(). El Waker es el mecanismo que notifica al executor: “Oye, esta tarea ya tiene los datos necesarios, vuelve a llamar a poll para ver si puede avanzar”. Sin este mecanismo, el executor no sabría cuándo despertar a la tarea y el programa se quedaría bloqueado o gastaría ciclos de CPU preguntando sin parar.

Un concepto crítico para entender la implementación de Future es Pin<&mut Self>. Muchos Future generados por bloques async son estructuras autorreferenciales (contienen punteros a sus propios campos internos para mantener el estado). Si estas estructuras se movieran de posición en la memoria, esos punteros quedarían invalidados, causando un comportamiento indefinido. Pin garantiza que el objeto se mantenga fijo en su ubicación de memoria, permitiendo que los punteros internos sean seguros.

Finalmente, es importante notar que async fn foo() -> T es simplemente azúcar sintáctica. El compilador la transforma internamente en algo equivalente a fn foo() -> impl Future<Output = T>, donde la función devuelve una máquina de estados optimizada para representar el flujo de ejecución.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker, RawWaker, RawWakerVTable};
use std::time::{Duration, Instant};
use std::thread;

/// Un Future manual que completa después de un número determinado de polls.
/// Esto nos permite entender la máquina de estados sin depender de un runtime complejo.
struct Countdown {
    remaining: u32,
}

impl Countdown {
    fn new(count: u32) -> Self {
        Self { remaining: count }
    }
}

impl Future for Countdown {
    type Output = &'static str;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.remaining == 0 {
            // La máquina de estados ha llegado a su estado final
            Poll::Ready("¡Contador finalizado!")
        } else {
            // Decrementamos el estado interno
            self.remaining -= 1;
            
            println!("Estado: faltan {} pasos...", self.remaining);

            // En un escenario real (ej. lectura de red), aquí registraríamos el 
            // waker en un reactor de eventos (como epoll o kqueue).
            // Como este es un ejemplo didáctico, simulamos que el waker 
            // es llamado inmediatamente para permitir que el executor siga.
            let waker = cx.waker().clone();
            waker.wake(); 

            Poll::Pending
        }
    }
}

fn main() {
    // --- Configuración de un Executor Minimalista ---
    // Para llamar a `poll` manualmente, necesitamos un `Waker` real.
    // Implementamos un "dummy waker" que no hace nada para simplificar el ejemplo.
    fn dummy_no_op(_: *const ()) {}
    fn dummy_clone(ptr: *const ()) -> RawWaker {
        RawWaker::from(ptr)
    }
    fn dummy_wake(_: *const ()) {}
    fn dummy_wake_by_ref(_: *const ()) {}

    let vtable = &RawWakerVTable::new(
        dummy_clone,
        dummy_clone,
        dummy_wake,
        dummy_wake_by_ref,
    );
    let raw_waker = RawWaker::from(std::ptr::null(), vtable);
    let waker = unsafe { Waker::from_raw(raw_waker) };
    let mut cx = Context::from_waker(&waker);
    // -----------------------------------------------

    let mut future = Countdown::new(3);

    // El "Executor" (el loop de polling)
    // En un runtime como Tokio, este loop es gestionado por el scheduler
    // y se queda durmiendo hasta que un `wake()` lo despierta.
    loop {
        // Usamos `as_mut()` para obtener un `Pin<&mut Self>` de nuestro struct
        match unsafe { Pin::new_unchecked(&mut future) }.poll(&mut cx) {
            Poll::Ready(resultado) => {
                println!("Resultado final: {}", resultado);
                break;
            }
            Poll::Pending => {
                // El executor podría hacer otras tareas aquí o dormir
                println!("El executor recibió Pending, esperando...");
            }
        }
    }
}

Análisis del código

  1. La Estructura Countdown: Define nuestro estado. En este caso, remaining: u32 actúa como nuestra variable de control de la máquina de estados.
  2. impl Future for Countdown:
    • type Output = &'static str;: Define el tipo de dato que devolverá el Future al completarse.
    • fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>): El método recibe self envuelto en un Pin. Esto es obligatorio para asegurar que la estructura Countdown no se mueva mientras es consultada.
    • self.remaining -= 1;: Modifica el estado interno de la máquina de estados cada vez que el executor pregunta por el progreso.
    • cx.waker().clone() y waker.wake(): Este es el punto más crítico. En el ejemplo, llamamos a wake() inmediatamente para que el loop en main no se quede bloqueado. En un caso real de I/O, llamarías a wake() solo cuando el hardware o el sistema operativo avisen que los datos están listos.
    • Poll::Pending: Indica que la tarea no ha terminado pero debe ceder el control.
  3. El Executor Manual en main:
    • RawWakerVTable: Para que el código sea compilable sin librerías externas, implementamos una tabla de funciones para el Waker. Esto es lo que sucede internamente en el núcleo de Rust para manejar punteros de tareas.
    • Context::from_waker(&waker): Crea el contexto necesario para pasarle a poll, permitiendo que la tarea tenga acceso al mecanismo de notificación.
    • El loop de polling: Representa el ciclo de vida de un executor. Llama a poll repetidamente. Si recibe Pending, la tarea sigue “viva” pero sin consumir tiempo de CPU (si el executor implementara un mecanismo de espera real). Si recibe Ready, el ciclo termina.

85