๐ŸŽ“ A1Learner
๐Ÿ”

Rust Your Own Future

A future written by hand is a struct that implements the Future trait itself. Its poll method reports Ready with a value or Pending for not yet.

The shape of the Future trait

Futures appeared with .await. The trait behind them is this small. It has one method, poll. poll answers Ready with the final value, or Pending for 'not yet'. (Simplified from the real std trait โ€” same shape.)

trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

A notebook shared with a thread

Our future is a timer called Delay. The sleeping happens on a second thread. Future and thread share a notebook: an Arc<Mutex<...>>. It holds a done flag and a place to leave the waker.

struct SharedState {
    completed: bool,
    waker: Option<Waker>,
}

struct Delay {
    state: Arc<Mutex<SharedState>>,
}

poll: not ready โ€” leave your number

If the sleep is not done, the future writes its waker into the notebook. It returns Pending โ€” 'call me, don't wait for me'. The executor moves on to other tasks.

impl Future for Delay {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        let mut state = self.state.lock().unwrap();
        if state.completed {
            Poll::Ready(())
        } else {
            state.waker = Some(cx.waker().clone());
            Poll::Pending
        }
    }
}

The thread does the sleeping

A second thread sleeps, flips the flag, and calls waker.wake() โ€” that one call is what makes the executor poll the future again. The second poll sees completed and returns Ready.

impl Delay {
    fn new(wait: Duration) -> Delay {
        let state = Arc::new(Mutex::new(SharedState { completed: false, waker: None }));
        let thread_state = Arc::clone(&state);
        thread::spawn(move || {
            thread::sleep(wait);
            let mut state = thread_state.lock().unwrap();
            state.completed = true;
            if let Some(waker) = state.waker.take() {
                waker.wake();
            }
        });
        Delay { state }
    }
}

Example

use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::thread;
use std::time::Duration;

struct SharedState {
    completed: bool,
    waker: Option<Waker>,
}

struct Delay {
    state: Arc<Mutex<SharedState>>,
}

impl Delay {
    fn new(wait: Duration) -> Delay {
        let state = Arc::new(Mutex::new(SharedState { completed: false, waker: None }));
        let thread_state = Arc::clone(&state);
        thread::spawn(move || {
            thread::sleep(wait);
            let mut state = thread_state.lock().unwrap();
            state.completed = true;
            if let Some(waker) = state.waker.take() {
                waker.wake();
            }
        });
        Delay { state }
    }
}

impl Future for Delay {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        let mut state = self.state.lock().unwrap();
        if state.completed {
            Poll::Ready(())
        } else {
            state.waker = Some(cx.waker().clone());
            Poll::Pending
        }
    }
}

fn main() {
    let mut delay = Delay::new(Duration::from_millis(30));
    let waker = Waker::noop();
    let mut cx = Context::from_waker(&waker);

    match Pin::new(&mut delay).poll(&mut cx) {
        Poll::Pending => println!("first poll: Pending"),
        Poll::Ready(()) => println!("first poll: Ready"),
    }

    thread::sleep(Duration::from_millis(50));

    match Pin::new(&mut delay).poll(&mut cx) {
        Poll::Pending => println!("second poll: Pending"),
        Poll::Ready(()) => println!("second poll: Ready"),
    }
}

Key points

  • A future is a type that implements Future.
  • Its one method, poll, asks 'are you done yet?' and answers Poll::Ready(value) or Poll::Pending.
  • poll must never block.
  • A blocking poll freezes the executor's thread for every other task.
  • The waiting itself must happen off the poll path.
  • Use a thread, an OS event, or a timer.
  • Store the waker on EVERY poll.
  • The executor may poll your future from a different task next time.
  • Refresh the saved Waker from cx.waker().clone() each time.
  • Pending is a promise, not a pause: return it only when something will call wake() later.
  • A Pending with no wake arranged is a task that sleeps forever.
  • Writing your own Future is rare in app code.
  • An async fn creates the same future automatically.
  • Timers, sockets, and channels are all hand-written futures.
  • Knowing the trait helps debug those futures.
  • The examples in this lesson use plain std only, with no crates.

You poll the future by hand here โ€” no executor at all. The first poll returns Pending and stores the waker in the notebook. Waker::noop() is a waker that does nothing when woken (stable since Rust 1.85), perfect for manual polling. The timer thread does run and 'wakes' it while we sleep. The second poll sees completed = true and returns Ready.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

A future returned Poll::Pending. What must happen before it is polled again?