Skip to content
Lesson 7 of 7

Step 1 of 7 · Reading · ~4 min

Learn

Lifetimes, Trait Objects, and Async

Futures, Wakers and Executors

You will be able to write a future by hand, state exactly what returning Pending promises, and drive futures with a small executor.

Trait objects gave you a vtable of methods and Unsafe gave you a vtable you build yourself. A Waker is that vtable, and a future is a trait object you can poll.

async fn is a state machine

This grader compiles the 2015 edition, where async fn is error[E0670], so this block is reading only. The rust blocks after it are fragments: put use std::{cell::{Cell, RefCell}, future::Future, pin::Pin, rc::Rc, task::*}; above them.

// not runnable here
async fn total(a: Gate, b: Gate) -> u32 {
    let x = a.await;
    let y = b.await;
    x + y
}

Calling total runs nothing; it builds a value. The compiler turns the body into an enum with one state per .await and implements Future for it. .await means: poll the inner future; if it is Pending, return Pending from my own poll and resume at this state next time; if Ready(v), continue with v.

poll, Poll and Pin

Future::poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> makes one step of progress and returns Poll::Ready(value) or Poll::Pending. The receiver is Pin<&mut Self> because a generated state machine can hold a reference to its own field across an .await, the self-referential struct lifetimes cannot express. Moving it would leave that reference dangling, so Pin forbids moves. Box::pin(x) fixes a value on the heap, and f.as_mut() reborrows the box as the Pin<&mut T> that poll takes.

Unpin is the auto trait for "moving this value is harmless". A struct of plain fields has it, so Pin<&mut Self> dereferences mutably and self.n -= 1 works; write mut self: Pin<&mut Self> in the signature to allow it.

The waker contract

Pending is a promise: "I am not ready, and I have arranged for cx.waker() to be woken when polling again is worthwhile." A future that only wants another turn keeps it with cx.waker().wake_by_ref() before returning Pending. A future waiting on an outside event stores a clone of the waker, and the event calls wake() later. A Waker is a data pointer plus a RawWakerVTable of four functions in this order: clone, wake, wake_by_ref, drop. This one counts wakes in a Cell<u32>, and wake and wake_by_ref share a function because the data is only a borrowed counter:

static VT: RawWakerVTable = RawWakerVTable::new(cl, wk, wk, dr);
unsafe fn cl(p: *const ()) -> RawWaker { RawWaker::new(p, &VT) }
unsafe fn wk(p: *const ()) { let c = &*(p as *const Cell<u32>); c.set(c.get() + 1); }
unsafe fn dr(_p: *const ()) {}

struct Shared { done: bool, waker: Option<Waker> }
struct Wait(Rc<RefCell<Shared>>);
impl Future for Wait {
    type Output = u32;
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<u32> {
        let mut s = self.0.borrow_mut();
        if s.done { return Poll::Ready(42); }
        s.waker = Some(cx.waker().clone());   // the promise
        Poll::Pending
    }
}

The driver builds a Context from a waker and polls through the pinned box; the block marked fire is the event:

let count = Cell::new(0u32);
let raw = RawWaker::new(&count as *const Cell<u32> as *const (), &VT);
let waker = unsafe { Waker::from_raw(raw) };
let mut cx = Context::from_waker(&waker);
let shared = Rc::new(RefCell::new(Shared { done: false, waker: None }));
let mut f = Box::pin(Wait(shared.clone()));
println!("poll 1: {:?}", f.as_mut().poll(&mut cx));
println!("wakes: {}", count.get());
{   // fire
    let mut s = shared.borrow_mut();
    s.done = true;
    if let Some(w) = s.waker.take() { w.wake(); }
}
println!("wakes: {}", count.get());
println!("poll 2: {:?}", f.as_mut().poll(&mut cx));
poll 1: Pending
wakes: 0
wakes: 1
poll 2: Ready(42)

Delete the line marked "the promise" and wakes stays 0 after fire: the task is lost.

Executors and cancellation

An executor owns Pin<Box<dyn Future<Output = T>>> tasks and must tell a woken Pending from a forgotten one, usually with a flag its waker sets. The flag must be cleared before each poll. With one shared flag, task A wakes and task B is silent:

Flag before pollPollFlag afterExecutor concludes
falseA: Pending, woketrueA is alive
true (stale)B: Pending, silenttrueB looks alive: wrong
false (cleared)B: Pending, silentfalseB is lost: right

Dropping a future is cancellation: its fields are dropped and poll is never called again, which is how a timeout abandons work. A task is one small state machine where a thread is a whole stack, so thousands of mostly-waiting jobs fit; a future that crosses threads must be Send.

Common mistakes

  • Returning Pending without waking: the task never runs again, and a loop that polls it anyway only hides the bug by spinning.
  • Not clearing the flag before each poll: a silent task inherits the previous wake and is requeued.
  • Putting a pending task at the front of the queue: it finishes before its neighbours get a turn.

Your exercise

Write the poll of a countdown future and the executor loop that runs a queue of them. A task that returns Pending without waking must be reported, not requeued; the likeliest slip is a stale flag.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…