Futures
At the highest level:
A
Futurerepresents a value that will be available later.
But in Rust, that idea is made very explicit and low-level.
A future is:
- lazy (nothing happens until it’s polled)
- stateful (it remembers where it left off)
- cooperatively scheduled (it must voluntarily yield)
The Future trait (real definition)
From std::future (simplified a bit):
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
This single method explains everything.
Understanding Poll
enum Poll<T> {
Ready(T),
Pending,
}
Ready(value)→ the future is completePending→ not ready yet, poll me again later
A future is allowed to return Pending as many times as it wants.
Futures are lazy (very important)
This code does nothing:
async fn compute() -> i32 {
5
}
fn main() {
let fut = compute();
}
Why?
compute()returns aFuture- That future is never polled
- So it never runs
In Rust:
Creating a future does not start execution
Who polls a Future
A runtime / executor (Tokio, async-std, etc.).
The executor:
- Calls
poll() - If
Pending, registers a waker - Moves on to other futures
- When woken, polls again
What is Context and Waker
The Context gives the future a way to say:
“Hey executor, wake me up when I can make progress.”
Simplified:
struct Context<'a> {
waker: &'a Waker,
}
Waker
- Stored by the future
- Used to notify the executor
- Called when external state changes (socket ready, timer fired, etc.)
A future is a state machine
Let’s build a tiny future by hand to see this clearly.
Example: a future that completes after being polled twice
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct TwoPollFuture {
polled_once: bool,
}
impl Future for TwoPollFuture {
type Output = &'static str;
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.polled_once {
Poll::Ready("done")
} else {
self.polled_once = true;
Poll::Pending
}
}
}
What’s happening
- First poll →
Pending - Second poll →
Ready("done") - Internal state (
polled_once) tracks progress
This is exactly what the compiler generates for async fn.
How async fn becomes a Future
async fn example() -> i32 {
let a = 1;
let b = async_call().await;
a + b
}
Roughly becomes:
enum ExampleState {
Start,
WaitingOnAsyncCall,
Done,
}
Each .await is:
- A possible
Pendingreturn - A resume point
.await is just polling in a loop
Conceptually:
loop {
match future.poll(cx) {
Poll::Ready(val) => break val,
Poll::Pending => return Pending,
}
}
.await:
- Polls the future
- Suspends if
Pending - Resumes at the same line later
Example: Custom future with a timer (Tokio)
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::time::{Instant, Sleep};
struct MySleep {
sleep: Pin<Box<Sleep>>,
}
impl Future for MySleep {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
self.get_mut().sleep.as_mut().poll(cx)
}
}
This wraps an existing future and delegates polling.
Using futures directly vs async/await
Using async/await (normal)
async fn work() -> i32 {
10
}
let result = work().await;
Using futures explicitly (rare, but educational)
use futures::executor::block_on;
let fut = work();
let result = block_on(fut);
Same thing. .await is just nicer syntax.
Futures are single-use
Once a future returns Ready:
- It must never be polled again
- Polling again is undefined behavior
That’s why futures are usually moved, not reused.
Why Pin<&mut Self>
Short version:
Futures must not move in memory once polling begins.
Because:
- They may store self-references
.awaitpoints depend on stable addresses
Pin enforces this at compile time.
Send futures and multithreading
Future + Send→ can move between threads!Sendfutures → must stay on one thread
Tokio:
spawnrequiresSendspawn_localdoes not
Key rules every future must follow
- Must not block
- Must return
Pendingwhen not ready - Must wake the executor when progress is possible
- Must not be polled after completion
- Must not move once pinned
Break these and async breaks 😅