Futures and a small executor
A future is a value with one method: poll takes a pinned reference and a context and returns either the value or the news that it is not ready. The context carries a waker, and the waker is how a future tells the executor to poll it again. An executor is the loop that calls poll and waits.
This lesson implements the trait twice by hand, builds a waker from Wake, parks a thread on a condition variable, and runs a timer without any crate. The last section is the compiler refusing a poll whose receiver is a plain reference.
The Future trait by hand
The associated type is what the future produces, and poll returns it as Poll::Ready or reports Poll::Pending. A future that is not ready must arrange for the waker to be called, which a countdown does not need because the driver keeps polling.
The countdown returns after four polls: three that report pending and one that returns the string. The program counts the calls.
use std::future::Future;
use std::pin::{pin, Pin};
use std::task::{Context, Poll, Waker};
struct Countdown {
left: u32,
}
impl Future for Countdown {
type Output = &'static str;
fn poll(mut self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<Self::Output> {
if self.left == 0 {
Poll::Ready("liftoff")
} else {
self.left -= 1;
Poll::Pending
}
}
}
fn main() {
let mut future = pin!(Countdown { left: 3 });
let waker = Waker::noop();
let mut context = Context::from_waker(waker);
let mut polls = 0;
loop {
polls += 1;
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => {
println!("{value} after {polls} polls");
break;
}
Poll::Pending => continue,
}
}
}
rustc --edition 2024 countdown.rs
./countdown
liftoff after 4 polls
The receiver is Pin<&mut Self> because a future that suspends keeps its state inside itself, and moving it between polls would move that state. For a struct of plain counters nothing would break, but the trait is written for the general case. The previous lesson is the syntax that builds such state machines automatically.
A waker and a parking thread
The executor of the first section spins on pending, which burns a core. A real one waits on the waker: the waker sets a flag and signals a condition variable, and the executor sleeps until that happens.
The Wake trait has one method, wake, which consumes the Arc; wake_by_ref is the version that keeps it. The program builds a waker from an Arc<Parker> and parks between polls.
use std::future::Future;
use std::pin::pin;
use std::sync::{Arc, Condvar, Mutex};
use std::task::{Context, Poll, Wake, Waker};
struct Parker {
ready: Mutex<bool>,
condvar: Condvar,
}
impl Parker {
fn new() -> Self {
Parker { ready: Mutex::new(false), condvar: Condvar::new() }
}
fn park(&self) {
let mut ready = self.ready.lock().unwrap();
while !*ready {
ready = self.condvar.wait(ready).unwrap();
}
*ready = false;
}
}
impl Wake for Parker {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
*self.ready.lock().unwrap() = true;
self.condvar.notify_one();
}
}
struct Timer(bool);
impl Future for Timer {
type Output = &'static str;
fn poll(mut self: std::pin::Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
if self.0 {
return Poll::Ready("done");
}
self.0 = true;
let waker = context.waker().clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(10));
waker.wake();
});
Poll::Pending
}
}
fn block_on<F: Future>(future: F) -> F::Output {
let mut future = pin!(future);
let parker = Arc::new(Parker::new());
let waker = Waker::from(Arc::clone(&parker));
let mut context = Context::from_waker(&waker);
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => return value,
Poll::Pending => parker.park(),
}
}
}
fn main() {
println!("{}", block_on(Timer(false)));
}
rustc --edition 2024 parker.rs
./parker
done
The timer returns pending, spawns a thread that sleeps and wakes the executor, and returns the string on the second poll. That is a timer with no runtime behind it, and the sleep is the only part that touches the clock. The lesson on condition variables is the park itself.
A future that wakes itself
A future that gives up the processor without waiting for anything calls wake_by_ref on its own waker and returns pending. The executor polls again at once, which is the cooperative yield that a long computation uses to stay responsive.
The program awaits a yield twice inside an async function and then returns forty-two. The same parker drives it, and the wake from inside the poll keeps the loop going.
use std::future::Future;
use std::pin::pin;
use std::sync::{Arc, Condvar, Mutex};
use std::task::{Context, Poll, Wake, Waker};
struct Parker {
ready: Mutex<bool>,
condvar: Condvar,
}
impl Parker {
fn new() -> Self {
Parker { ready: Mutex::new(false), condvar: Condvar::new() }
}
fn park(&self) {
let mut ready = self.ready.lock().unwrap();
while !*ready {
ready = self.condvar.wait(ready).unwrap();
}
*ready = false;
}
}
impl Wake for Parker {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
*self.ready.lock().unwrap() = true;
self.condvar.notify_one();
}
}
fn block_on<F: Future>(future: F) -> F::Output {
let mut future = pin!(future);
let parker = Arc::new(Parker::new());
let waker = Waker::from(Arc::clone(&parker));
let mut context = Context::from_waker(&waker);
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => return value,
Poll::Pending => parker.park(),
}
}
}
struct YieldNow(bool);
impl Future for YieldNow {
type Output = ();
fn poll(mut self: std::pin::Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
if self.0 {
return Poll::Ready(());
}
self.0 = true;
context.waker().wake_by_ref();
Poll::Pending
}
}
async fn work() -> i32 {
YieldNow(false).await;
YieldNow(false).await;
42
}
fn main() {
println!("{}", block_on(work()));
}
rustc --edition 2024 yield.rs
./y
42
The value is the same as a plain return, and the two suspensions happen in between. A crate that runs many tasks puts the same futures in a queue and polls whichever one was woken, which is what tokio does; the program above is that loop with one task and no reactor.
An executor is three things: a queue of tasks, a waker per task that puts the task back in the queue, and a reactor that calls those wakers when a socket or a timer is ready. The parker above is the simplest possible version of the second and none of the third.
tokio is the crate most programs use for all three. It brings a multi-threaded scheduler, a timer, the network reactor and the macros that start a runtime, and it is the reason an async program on a network socket never calls a blocking read. The executor of this lesson is enough to understand what happens under those macros, and not enough to run a server.
The future returned by an async function is the same trait the two hand-written types implement, so an await works on either. The next lesson turns the state machine of a future into an iterator that yields several values.
A poll with the wrong receiver
The trait declares poll(self: Pin<&mut Self>, ...), and an implementation with a plain &mut self does not match. The compiler reports E0053 and prints both signatures.
The program implements a ready future with the wrong receiver. The signature in the error is the one to copy.
use std::future::Future;
use std::task::{Context, Poll};
struct Ready7;
impl Future for Ready7 {
type Output = i32;
fn poll(&mut self, _context: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(7)
}
}
fn main() {}
rustc --edition 2024 poll_err.rs
error[E0053]: method `poll` has an incompatible type for trait
--> poll_err.rs:9:13
|
9 | fn poll(&mut self, _context: &mut Context<'_>) -> Poll<Self::Output> {
| ^^^^^^^^^ expected `Pin<&mut Ready7>`, found `&mut Ready7`
|
= note: expected signature `fn(Pin<&mut Ready7>, &mut Context<'_>) -> Poll<_>`
found signature `fn(&mut Ready7, &mut Context<'_>) -> Poll<_>`
help: change the self-receiver type to match the trait
|
9 - fn poll(&mut self, _context: &mut Context<'_>) -> Poll<Self::Output> {
9 + fn poll(self: Pin<&mut Ready7>, _context: &mut Context<'_>) -> Poll<Self::Output> {
|
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0053`.
The help is a compilable line, and the body then needs self.get_mut() or self.left through the pin for a field. The lesson on manual memory is the reason the pin exists: a future must not move while it is suspended.
Exercises
- Give the countdown a waker call so the spinning driver can park between polls.
- Write a future that returns the sum of three polls and count how often the executor polled it.
- Print how long the timer took with Instant and explain the number.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
Future::pollreturn?Answer
Poll::Ready with the value or Poll::Pending
The two variants are the whole protocol between the future and the executor.
-
What is the waker for?
Answer
It tells the executor to poll the future again
A future that is not ready wakes the executor when it has news.
-
What must a polling loop do when a future is pending?
Answer
Wait for the waker instead of spinning
The waker is the signal that makes waiting possible without burning a core.
-
What does an executor add to a waker?
Answer
A queue of tasks and a reactor for timers and sockets
The waker is the mechanism; the scheduler and the reactor are the runtime.
-
What does this program print?
use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; struct Ready; impl Future for Ready { type Output = i32; fn poll(self: std::pin::Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<Self::Output> { Poll::Ready(9) } } fn main() { let mut future = pin!(Ready); let waker = Waker::noop(); let mut context = Context::from_waker(waker); if let Poll::Ready(value) = future.as_mut().poll(&mut context) { println!("{value}"); } }Answer
9
The first poll is ready, and the value is printed.
-
What does this program print?
use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; struct Step(u8); impl Future for Step { type Output = u8; fn poll(mut self: std::pin::Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<Self::Output> { self.0 += 1; if self.0 == 3 { Poll::Ready(self.0) } else { Poll::Pending } } } fn main() { let mut future = pin!(Step(0)); let waker = Waker::noop(); let mut context = Context::from_waker(waker); loop { if let Poll::Ready(value) = future.as_mut().poll(&mut context) { println!("{value}"); break; } } }Answer
3
The third poll reaches three and returns it.
-
What does this program print?
use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; struct Count(u8); impl Future for Count { type Output = u8; fn poll(mut self: std::pin::Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> { self.0 += 1; if self.0 < 2 { context.waker().wake_by_ref(); Poll::Pending } else { Poll::Ready(self.0) } } } fn main() { let mut future = pin!(Count(0)); let waker = Waker::noop(); let mut context = Context::from_waker(waker); loop { if let Poll::Ready(value) = future.as_mut().poll(&mut context) { println!("{value}"); break; } } }Answer
2
The first poll yields and the second returns two.
-
What does this program print?
use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Wake, Waker}; use std::sync::Arc; struct Flag; impl Wake for Flag { fn wake(self: Arc<Self>) { println!("woken"); } } fn main() { let waker = Waker::from(Arc::new(Flag)); let mut future = pin!(Box::pin(async { 5 })); let mut context = Context::from_waker(&waker); if let Poll::Ready(value) = future.as_mut().as_mut().poll(&mut context) { println!("{value}"); } }Answer
5
The async block is ready at once, so the waker is never used and only the value prints.
-
What is the first line of the compiler output for this program?
use std::future::Future; use std::task::{Context, Poll}; struct Ready7; impl Future for Ready7 { type Output = i32; fn poll(&mut self, _context: &mut Context<'_>) -> Poll<Self::Output> { Poll::Ready(7) } } fn main() {}Answer
error[E0053]: method
pollhas an incompatible type for traitThe trait takes Pin<&mut Self>, and a plain &mut self does not match the signature.
-
What is the first line of the compiler output for this program?
use std::future::Future; struct Ready; impl Future for Ready { type Output = i32; fn poll(self: std::pin::Pin<&mut Self>, _context: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> { std::task::Poll::Ready(1) } } fn main() { let value = Ready.await; println!("{value}"); }Answer
error[E0728]:
awaitis only allowed insideasyncfunctions and blocksEven a hand-written future needs an async context or a driver for await.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev