poll: Ready con el valor o Pending con un todavía no
El runtime conduce un future preguntándole en bucle: llama a poll y recibe uno de dos veredictos, Poll::Ready con el valor terminado o Poll::Pending que significa 'aún no'. Diseccionamos la firma de poll símbolo a símbolo y el contrato del protocolo: por qué Pending no es 'vuelve enseguida' y por qué sondear tras Ready rompe el pacto.
Un future no se ejecuta: se conduce. Y conducirlo consiste en una sola operación repetida —poll— cuya respuesta es siempre uno de dos veredictos. O bien Poll::Ready(v), “he terminado, aquí tienes el valor”, o bien Poll::Pending, “todavía no”. Toda la programación asíncrona de Rust, por debajo de la sintaxis async/.await, es este diálogo escueto entre un runtime que pregunta “¿ya?” y un future que responde con el resultado o con una promesa de avisar. En esta lección abrimos la firma de poll símbolo a símbolo y, sobre todo, el protocolo que la gobierna: un contrato tácito cuyo incumplimiento no lo atrapa el compilador, pero sí arruina el programa.
- Leer el
enum Polly el significado exacto de sus variantesReadyyPending. - Diseccionar la firma de
poll: el receptorPin<&mut Self>, elContexty el retornoPoll. - Entender el protocolo de sondeo:
Pendingobliga a avisar; trasReadyno se vuelve a sondear. - Distinguir por qué
Pendingno significa “reintenta ya”, sino “no vuelvas hasta que te llame”.
El enum Poll: dos veredictos y nada más
El valor que devuelve poll es de un tipo sorprendentemente humilde, un enum de dos variantes:
pub enum Poll<T> {
Ready(T), // terminado: aqui esta el valor de tipo T
Pending, // todavia no hay resultado
}
Eso es todo lo que un future puede contestar. Poll::Ready(v) transporta el resultado —el mismo tipo T que el Output del future— y sella el cálculo: ya está. Poll::Pending no lleva nada; es la palabra “aún no”, un veredicto sin carga útil. No hay una tercera variante para errores: un future falible simplemente tiene Output = Result<T, E>, de modo que el error viaja dentro del Ready. Fallar es haber terminado con un Err, no una forma distinta de estar pendiente.
La economía del enum es deliberada. Sondear un future es preguntar por su estado en un instante, y solo hay dos estados posibles que le importen a quien lo conduce: ha producido el valor o no lo ha producido. Cualquier matiz adicional —por qué no está listo, cuánto falta— sería información que el runtime no necesita para hacer su trabajo, que es decidir entre “recojo el resultado” y “aparco esta tarea”.
Un matiz importante: los futures falibles no estrenan una tercera variante. El error viaja dentro del Ready, porque fallar es una forma de terminar.
// type Output = Result<Vec<u8>, std::io::Error>;
// Poll::Ready(Ok(datos)) -> termine bien
// Poll::Ready(Err(error)) -> termine mal, pero termine
// Poll::Pending -> aun no se el desenlace
Poll::Ready(v)
Terminó. Transporta el valor de tipo Output —un Err incluido, si el future es falible—. Sella el cálculo: no se vuelve a sondear.
Poll::Pending
Todavía no. No lleva dato. Es un compromiso: el future ya dispuso que el Waker lo avise cuando valga la pena reintentar.
La firma de poll, símbolo a símbolo
Recuperemos la firma exacta y leamos cada pieza:
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
self: Pin<&mut Self>— el receptor. Necesita&mut Selfporque sondear avanza la máquina de estados: cadapollpuede mutar el estado interno del future. El envoltorioPines la promesa, estudiada en el nivel 28, de que la dirección del future no cambiará entre sondeos; hace falta porque la máquina puede guardar referencias a sí misma, y moverla las invalidaría. Por eso no recibes un&mut Selfdesnudo sino uno fijado.cx: &mut Context<'_>— el contexto de la llamada. Hoy su única carga es elWaker, accesible concx.waker(): el mango que el future usará, si devuelvePending, para pedir que lo vuelvan a sondear cuando haya progreso. Es el tema de la lección siguiente; aquí basta saber quepollsiempre recibe un canal para decir “avísame”.-> Poll<Self::Output>— el veredicto:Readycon elOutputdel future, oPending.
Fíjate en la asimetría: poll recibe todo lo necesario para intentar avanzar (acceso mutable fijado, un waker que registrar) y devuelve lo mínimo para informar del resultado (terminado o no). No hay parámetros de tiempo, ni de reintentos, ni de prioridad. Esa desnudez es lo que permite que el mismo trait sirva a un runtime gigante multihilo y a un bucle block_on de veinte líneas.
La firma de poll intimida, pero en el día a día no la tocas: la sintaxis async/.await genera el poll por ti, con su Pin, su Context y su match de estados. Escribir poll a mano se reserva para futures hoja —los que hablan directamente con el sistema o con hardware— y para entender el modelo, que es lo que haremos en la lección cinco. El 99 % del código async compone futures existentes con .await y nunca ve esta firma. Conocerla, sin embargo, es lo que convierte el .await de conjuro en mecanismo.
El protocolo: preguntar hasta Ready
poll no se llama una vez: se llama las veces que haga falta hasta arrancarle un Ready. Ese bucle de preguntas obedece un contrato con dos cláusulas que el compilador no vigila y que, por tanto, debes respetar tú al implementar un future:
- Si devuelves
Pending, te comprometes a que alguien llame alWaker. No es opcional. DevolverPendingsin haber dispuesto un aviso significa que el runtime aparcará la tarea y jamás la volverá a sondear: se cuelga para siempre, en silencio.Pendinges una promesa, no una excusa. - Una vez que devuelves
Ready, no se te debe volver a sondear. El future ya entregó su valor y puede haber consumido su estado; sondearlo de nuevo es comportamiento indefinido a nivel de contrato —muchos futures hacenpanic!con “polled after completion”—. Un future se completa una vez.
De la primera cláusula sale la lección clave sobre el significado de Pending. No quiere decir “vuelve a preguntarme enseguida”, sino justo lo opuesto: “no vuelvas hasta que yo te avise”. Un runtime que, al recibir Pending, reintentara poll en un bucle cerrado estaría haciendo espera activa —quemando un núcleo preguntando “¿ya? ¿ya? ¿ya?”—, que es precisamente lo que el modelo evita. El runtime correcto aparca la tarea y se duerme; solo la resucita cuando el Waker dispara.
// Esqueleto de un poll que respeta el protocolo:
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
if self.hay_resultado() {
Poll::Ready(self.tomar_resultado()) // termino: entrega el valor
} else {
self.registrar_waker(cx.waker()); // CLAUSULA 1: dispon el aviso...
Poll::Pending // ...y solo entonces cede
}
}
Visto desde el otro lado, el del runtime que conduce, sondear “hasta Ready” es literalmente un bucle. Un block_on mínimo —el executor más simple que existe— sondea y, ante Pending, aparca el hilo:
use std::pin::pin;
use std::task::Context;
fn block_on<F: Future>(futuro: F) -> F::Output {
let mut futuro = pin!(futuro); // fija el future (Rust 2024)
let waker = waker_que_desbloquea_este_hilo();
let mut cx = Context::from_waker(&waker);
loop {
match futuro.as_mut().poll(&mut cx) {
Poll::Ready(v) => return v, // termino: sal del bucle
Poll::Pending => aparcar_hasta_wake(), // duerme; el waker despierta
}
}
}
No hay más misterio en “conducir un future”: preguntar poll, y ante Pending, dormir el hilo hasta que el Waker lo desbloquee y reintentar. Un runtime real como Tokio hace esto mismo multiplicado por miles de tareas sobre varios hilos, pero el corazón es este bucle.
Ninguna de las dos violaciones del contrato la detiene el compilador, y ambas producen fallos difíciles de diagnosticar. Sondear un future después de que devolvió Ready suele reventar con un panic de “future polled after completion”, porque su estado quedó consumido. Y devolver Pending sin registrar el Waker no revienta: hace algo peor, cuelga la tarea sin ruido, porque nadie la volverá a encolar. Cuando una tarea async “se queda muerta” sin panic ni error, sospecha siempre de un Pending que olvidó su waker. Son las dos reglas de oro de quien implementa futures a mano.
flowchart TD
P[El runtime llama a poll] --> V{Que veredicto}
V -->|Ready v| DONE[Recoge el valor y no vuelve a sondear]
V -->|Pending| W[El future registro el waker]
W --> SLEEP[El runtime aparca la tarea y duerme]
SLEEP -->|el waker dispara| P
style P fill:#cba6f7,color:#11111b
style DONE fill:#a6e3a1,color:#11111b
style SLEEP fill:#fab387,color:#11111bDetrás de la humildad de un enum de dos variantes se esconde una elección arquitectónica que define cómo Rust reparte el tiempo. En el modelo de hilos del sistema, la planificación es preventiva: el kernel puede arrebatarle el procesador a un hilo en cualquier instante, entre dos instrucciones cualesquiera, sin que el hilo lo sepa ni consienta. Es cómodo —el programador no piensa en ceder— pero caro: cada cambio de contexto cruza al núcleo, salva registros, ensucia cachés, y ocurre a ciegas, sin que el planificador sepa nada de lo que el programa intenta hacer. El modelo poll invierte la relación: la planificación se vuelve cooperativa y se muda al espacio de usuario. Un future solo cede en los puntos donde él mismo devuelve Pending, y solo entonces; entre dos Pending corre de un tirón, sin que nadie lo interrumpa. poll es el átomo de ese esquema: la unidad indivisible de “intenta progresar un poco”. El runtime no fuerza el avance, lo solicita, y el future responde con la única información que la planificación cooperativa necesita —terminé o no terminé— más, implícito en el Pending, un compromiso de avisar cuando valga la pena reintentar. Que el veredicto sea tan pobre no es una carencia sino la clave de la generalidad: como poll no habla de tiempo, ni de reintentos, ni de prioridades, el mismo trait Future lo puede conducir un executor de trabajo robado con ocho hilos, un bucle monohilo en un microcontrolador, o un test que sondea a mano paso a paso. Toda la política —cuándo dormir, a quién despertar antes, cómo repartir tareas entre hilos— vive en el runtime; el trait solo define el mecanismo mínimo, la pregunta y sus dos respuestas. Y el contrato tácito que lo acompaña —tras Ready no vuelvas, con Pending avisa— es el pacto que hace cooperar a millones de tareas sin un árbitro central que las vigile. Cuando entiendes que .await es azúcar sobre un bucle que llama a poll y cede en cada Pending, dejas de ver la concurrencia async como hilos disfrazados y empiezas a verla como lo que es: una danza de cesiones voluntarias coordinada por un enum de dos casos.
poll devuelve un Poll<T>: Ready(v) con el valor terminado, o Pending sin carga —el error de un future falible viaja dentro del Ready como Err, no como tercera variante—. Su firma es poll(self: Pin<&mut Self>, cx: &mut Context<'_>): Pin<&mut Self> da acceso mutable fijado para avanzar la máquina, y cx transporta el Waker. El protocolo tiene dos reglas no vigiladas por el compilador: con Pending debes registrar el waker o la tarea se cuelga en silencio, y tras Ready no se vuelve a sondear. Por eso Pending significa “no vuelvas hasta que te avise”, no “reintenta ya”: el runtime aparca y duerme en vez de girar.
- Escribe el
enum Poll<T>de memoria y explica por qué un future que puede fallar usaOutput = Result<T, E>en lugar de una tercera variantePoll::Error. - Enumera las tres piezas de la firma de
polly di qué aporta cada una; paraPin<&mut Self>, conecta con lo aprendido en el nivel 28 sobre por qué la máquina no debe moverse. - Implementa un
pollque devuelvaReady(7)la primera vez; sondéalo dos veces y observa qué pasa si tu segundo sondeo asume que el estado seguía intacto. Relaciónalo con la regla “tras Ready no vuelvas”. - Explica, con el diagrama de esta lección, por qué un runtime que hiciera
pollen bucle ante cadaPendingsería una espera activa y qué recurso concreto malgastaría. - Argumenta por qué la pobreza informativa de
Poll—solo dos casos, sin datos de tiempo ni prioridad— es la que permite que un mismoFuturelo conduzcan runtimes tan dispares como Tokio y unblock_oncasero.