Lesson 4 of 44
Futures and async/await
This lesson explains what an async fn returns, what .await does, and what the compiler builds from async code. Afterwards you can read a future as a state machine, write a small Future by hand, box futures of different types, and read the compiler errors that async code produces most often.
| Idea | Examples |
|---|---|
async fn returns a future, laziness |
1, 16 |
Future, poll, Poll::Ready, Poll::Pending |
2, 3, 4 |
.await, a runtime polling for you, pending propagation |
5, 6 |
async blocks, async move |
7 |
Futures as state machines, size_of_val |
8, 9 |
| Sequential and concurrent awaiting | 10 |
| Boxed futures, recursion | 11, 12 |
Send futures, guards across .await |
13 |
| Passing async work to a function | 14 |
.await outside async |
15 |
Example 1: async fn returns a future
Calling an async fn does not run its body. It returns a future, a value that describes a computation that has not finished. The body runs only when something drives the future.
async fn greet() -> String {
println!("body of greet runs");
"hello".to_string()
}
#[tokio::main]
async fn main() {
println!("calling greet");
let future = greet();
println!("greet returned a future, the body has not run");
let text = future.await;
println!("result: {text}");
}calling greet
greet returned a future, the body has not run
body of greet runs
result: helloThe line from the body of greet appears after the second println!, at the .await. .await waits for the future to finish and produces its output. It can appear only inside an async function or block (Example 15).
Example 2: The Future trait
Every future implements std::future::Future. This is the definition from the standard library:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
pub enum Poll<T> {
Ready(T),
Pending,
}The block is not compiled because it redeclares standard-library items. The parts:
| Part | Meaning |
|---|---|
Output |
the type of the value the future produces |
poll |
asks the future to make progress |
Poll::Ready(value) |
the future has finished and produced value |
Poll::Pending |
the future cannot finish yet; it has arranged to be polled again later |
Context |
carries a Waker, the handle a future uses to say "poll me again" |
Pin<&mut Self> |
a mutable reference that promises the future will not be moved in memory |
Context, Waker, and Pin are explained in Polling, Wakers, and a Tiny Executor. This lesson only needs them as arguments to poll.
Example 3: Polling a future by hand
A runtime normally calls poll. To see the API, the program below calls it directly. It builds a Context from Waker::noop(), a waker that does nothing, and pins the future on the stack with std::pin::pin!. std::future::ready is a future that is finished the first time it is polled.
use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, Waker};
fn main() {
let mut cx = Context::from_waker(Waker::noop());
let mut future = pin!(std::future::ready(42));
match future.as_mut().poll(&mut cx) {
Poll::Ready(value) => println!("ready with {value}"),
Poll::Pending => println!("pending"),
}
}ready with 42The first poll returns Ready(42). A future must not be polled again after it returns Ready; the standard library allows it to panic or misbehave.
Example 4: A future that is pending, then ready
A future that needs time returns Pending first. The Countdown below returns Pending a fixed number of times and then Ready. Before returning Pending it calls cx.waker().wake_by_ref(), which tells the runtime "poll me again". Without that call a real runtime would never poll the future again (Polling, Wakers, and a Tiny Executor shows the failure).
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>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.left == 0 {
Poll::Ready("liftoff")
} else {
self.left -= 1;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
fn main() {
let mut cx = Context::from_waker(Waker::noop());
let mut future = pin!(Countdown { left: 2 });
for attempt in 1..=4 {
println!("poll {attempt}: {:?}", future.as_mut().poll(&mut cx));
}
}poll 1: Pending
poll 2: Pending
poll 3: Ready("liftoff")
poll 4: Ready("liftoff")The third poll returns Ready. The fourth poll, made only to show the point, is outside the contract: Countdown happens to keep returning Ready, but a future is allowed to panic there.
Example 5: A runtime polls for you
Under #[tokio::main], .await hands the future to the runtime, which calls poll until the result is Ready. The Countdown from Example 4 works unchanged. The counter shows how many polls the runtime made.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct Countdown {
left: u32,
polls: u32,
}
impl Future for Countdown {
type Output = u32;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
self.polls += 1;
if self.left == 0 {
Poll::Ready(self.polls)
} else {
self.left -= 1;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
#[tokio::main]
async fn main() {
let polls = Countdown { left: 3, polls: 0 }.await;
println!("the runtime polled {polls} times");
}the runtime polled 4 timesThree Pending results and one Ready make four polls. The code that awaits the future sees none of this: .await returns only the final value.
Example 6: .await passes Pending upward
An async fn is itself a future. When it reaches an .await on an inner future that returns Pending, the outer future returns Pending too. The outer future resumes at the same .await on the next poll.
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 = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if self.left == 0 {
Poll::Ready(())
} else {
self.left -= 1;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
async fn job() -> &'static str {
println!("job: start");
Countdown { left: 2 }.await;
println!("job: countdown finished");
"job result"
}
fn main() {
let mut cx = Context::from_waker(Waker::noop());
let mut future = pin!(job());
let mut polls = 0;
let result = loop {
polls += 1;
match future.as_mut().poll(&mut cx) {
Poll::Ready(value) => break value,
Poll::Pending => println!("poll {polls}: pending"),
}
};
println!("poll {polls}: ready with {result:?}");
}job: start
poll 1: pending
poll 2: pending
job: countdown finished
poll 3: ready with "job result"job: start prints once, during the first poll. The body does not restart after each Pending: the future remembers where it stopped. Example 8 shows how.
Example 7: async blocks and async move
An async { ... } block is a future written inline, with no function around it. Like an async fn, it does not run until awaited.
#[tokio::main]
async fn main() {
let name = String::from("Ada");
let by_reference = async { format!("hello {name}") };
println!("{}", by_reference.await);
println!("name is still usable: {name}");
let by_move = async move { format!("goodbye {name}") };
println!("{}", by_move.await);
}hello Ada
name is still usable: Ada
goodbye AdaAn async block borrows the variables it uses, like a closure. async move takes ownership, again like move ||. The difference matters when the future has to outlive the function that created it. This function returns a future that borrows a local String, and the compiler rejects it:
use std::future::Future;
fn length_later() -> impl Future<Output = usize> {
let text = String::from("abc");
async { text.len() }
}
fn main() {
let _ = length_later();
}error[E0373]: async block may outlive the current function, but it borrows `text`, which is owned by the current function
--> src/main.rs:5:5
|
5 | async { text.len() }
| ^^^^^ ---- `text` is borrowed here
| |
| may outlive borrowed value `text`
|
note: async block is returned here
--> src/main.rs:5:5
|
5 | async { text.len() }
| ^^^^^^^^^^^^^^^^^^^^
help: to force the async block to take ownership of `text` (and any other referenced variables), use the `move` keyword
|
5 | async move { text.len() }
| ++++async move fixes it, because the block then owns text:
use std::future::Future;
fn length_later() -> impl Future<Output = usize> {
let text = String::from("abc");
async move { text.len() }
}
#[tokio::main]
async fn main() {
println!("{}", length_later().await);
}3Example 8: A future is a state machine
The compiler turns each async fn into a value of an anonymous type that implements Future. That value stores the state of the function: which .await it is waiting at, and the local variables that are still needed after that point. This is a state machine.
The program below writes by hand what the compiler generates for two_ticks. Tick is a small future that returns Pending once and then Ready with its id.
use std::future::Future;
use std::pin::{Pin, pin};
use std::task::{Context, Poll, Waker};
struct Tick {
pending_left: u32,
id: u32,
}
impl Future for Tick {
type Output = u32;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
if self.pending_left == 0 {
Poll::Ready(self.id)
} else {
self.pending_left -= 1;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
// The async version.
async fn two_ticks() -> u32 {
let a = Tick { pending_left: 1, id: 10 }.await;
println!("first tick finished: {a}");
let b = Tick { pending_left: 1, id: 20 }.await;
a + b
}
// The hand-written state machine.
enum TwoTicks {
WaitingFirst(Tick),
WaitingSecond { a: u32, tick: Tick },
Finished,
}
impl Future for TwoTicks {
type Output = u32;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
loop {
match &mut *self {
TwoTicks::WaitingFirst(tick) => match Pin::new(tick).poll(cx) {
Poll::Ready(a) => {
println!("first tick finished: {a}");
*self = TwoTicks::WaitingSecond {
a,
tick: Tick { pending_left: 1, id: 20 },
};
}
Poll::Pending => return Poll::Pending,
},
TwoTicks::WaitingSecond { a, tick } => match Pin::new(tick).poll(cx) {
Poll::Ready(b) => {
let sum = *a + b;
*self = TwoTicks::Finished;
return Poll::Ready(sum);
}
Poll::Pending => return Poll::Pending,
},
TwoTicks::Finished => panic!("polled after completion"),
}
}
}
}
fn drive<F: Future<Output = u32>>(name: &str, future: F) {
let mut cx = Context::from_waker(Waker::noop());
let mut future = pin!(future);
let mut polls = 0;
let result = loop {
polls += 1;
if let Poll::Ready(value) = future.as_mut().poll(&mut cx) {
break value;
}
};
println!("{name}: result {result} after {polls} polls");
}
fn main() {
drive("async fn", two_ticks());
drive("by hand", TwoTicks::WaitingFirst(Tick { pending_left: 1, id: 10 }));
}first tick finished: 10
async fn: result 30 after 3 polls
first tick finished: 10
by hand: result 30 after 3 pollsBoth versions print the same lines and finish after the same number of polls. Each .await in the async fn corresponds to one variant of the enum, and a local variable that lives across an .await (a) is a field of that variant. The real generated type is more complex, but it follows this shape. Writing Futures and Streams by Hand builds such types in more detail.
Example 9: The size of a future
A future stores the locals that live across an .await, so its size depends on them. std::mem::size_of_val reports the size of a value, including a future, without running it. Sizes are properties of the compiler version (Rust 1.94 for these outputs).
use std::future::Future;
use std::mem::size_of_val;
use std::pin::Pin;
use std::task::{Context, Poll};
struct YieldOnce(bool);
impl Future for YieldOnce {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if self.0 {
Poll::Ready(())
} else {
self.0 = true;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
async fn no_locals() {
YieldOnce(false).await;
}
async fn keeps_buffer_across_await() {
let buffer = [0u8; 1024];
YieldOnce(false).await;
println!("{}", buffer.len());
}
async fn drops_buffer_before_await() {
{
let buffer = [0u8; 1024];
println!("{}", buffer.len());
}
YieldOnce(false).await;
}
async fn awaits_another_future() {
keeps_buffer_across_await().await;
}
fn main() {
println!("no locals: {}", size_of_val(&no_locals()));
println!("buffer across await: {}", size_of_val(&keeps_buffer_across_await()));
println!("buffer dropped before: {}", size_of_val(&drops_buffer_before_await()));
println!("awaits the larger future: {}", size_of_val(&awaits_another_future()));
}no locals: 2
buffer across await: 1026
buffer dropped before: 2
awaits the larger future: 1027The future that holds a 1024-byte array across an .await is larger than 1024 bytes. The future that drops the array before the .await does not store it. A future that awaits another future contains that future, so it is at least as large. Large futures matter for task spawning cost; Anatomy of a Task returns to this.
Example 10: Sequential and concurrent awaiting
Awaiting two futures one after the other runs them one after the other. Running them at the same time needs a construct that polls both. tokio::join! is one: it polls several futures from the same task until all are finished and returns all their outputs (join!, try_join!, and Running Work Concurrently covers it in full).
Steps below prints a line each time it is polled and returns Pending until its steps run out, so the output shows the order of the polls.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct Steps {
name: &'static str,
left: u32,
}
impl Future for Steps {
type Output = &'static str;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.left == 0 {
println!("{}: finished", self.name);
Poll::Ready(self.name)
} else {
println!("{}: step, {} left", self.name, self.left);
self.left -= 1;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
#[tokio::main]
async fn main() {
println!("-- sequential");
Steps { name: "a", left: 2 }.await;
Steps { name: "b", left: 2 }.await;
println!("-- concurrent");
let (x, y) = tokio::join!(
biased;
Steps { name: "a", left: 2 },
Steps { name: "b", left: 2 },
);
println!("results: {x} {y}");
}-- sequential
a: step, 2 left
a: step, 1 left
a: finished
b: step, 2 left
b: step, 1 left
b: finished
-- concurrent
a: step, 2 left
b: step, 2 left
a: step, 1 left
b: step, 1 left
a: finished
b: finished
results: a bSequential awaiting finishes a before b starts. With join!, the two futures take turns: while one is Pending, the other gets polled. Both still run on the same thread, inside one task. Concurrency here means overlapping waiting, not parallel execution.
The biased; line makes join! poll its futures from top to bottom on every round. By default join! rotates which future it polls first each time it is woken, so that no future is always first (Tokio documentation, section "Fairness"). The rotation changes the order of the lines in the output but not the results.
Example 11: Boxed futures
Every async fn has its own anonymous future type, so two different async fn values cannot go in the same Vec or be the two branches of an if. Boxing erases the difference. Pin<Box<dyn Future<Output = T>>> is the type of a heap-allocated future of any concrete type with output T. Box::pin(future) creates one. (Polling, Wakers, and a Tiny Executor explains why Pin is in the type. For now it is part of the spelling.)
use std::future::Future;
use std::pin::Pin;
type BoxFuture<T> = Pin<Box<dyn Future<Output = T>>>;
async fn double(n: u32) -> u32 {
n * 2
}
async fn add_ten(n: u32) -> u32 {
n + 10
}
fn choose(use_double: bool, n: u32) -> BoxFuture<u32> {
if use_double {
Box::pin(double(n))
} else {
Box::pin(add_ten(n))
}
}
#[tokio::main]
async fn main() {
let mut jobs: Vec<BoxFuture<u32>> = Vec::new();
jobs.push(Box::pin(double(4)));
jobs.push(Box::pin(add_ten(4)));
jobs.push(choose(true, 100));
jobs.push(Box::pin(async { 7 }));
for job in jobs {
println!("{}", job.await);
}
}8
14
200
7Without the box, the if in choose fails because its two branches have different types. Boxing costs one heap allocation per future.
Example 12: Recursion needs a box
A recursive async fn would contain itself, and a type cannot have infinite size. The compiler rejects it with E0733:
async fn count_down(n: u32) -> u32 {
if n == 0 { 0 } else { 1 + count_down(n - 1).await }
}
fn main() {
let _ = count_down(3);
}error[E0733]: recursion in an async fn requires boxing
--> src/main.rs:1:1
|
1 | async fn count_down(n: u32) -> u32 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | if n == 0 { 0 } else { 1 + count_down(n - 1).await }
| ----------------------- recursive call here
|
= note: a recursive `async fn` call must introduce indirection such as `Box::pin` to avoid an infinitely sized futureReturning a boxed future breaks the cycle, because a Box has a fixed size:
use std::future::Future;
use std::pin::Pin;
fn count_down(n: u32) -> Pin<Box<dyn Future<Output = u32>>> {
Box::pin(async move {
if n == 0 { 0 } else { 1 + count_down(n - 1).await }
})
}
#[tokio::main]
async fn main() {
println!("depth = {}", count_down(5).await);
}depth = 5Example 13: Send futures and guards across .await
Send is the marker trait for types that may be moved to another thread. The multi-thread runtime moves tasks between worker threads, so a task's future must be Send (Spawning Tasks applies this to tokio::spawn). A future is Send only if every value it keeps across an .await is Send.
std::sync::MutexGuard is not Send. Holding one across an .await makes the future not Send. The helper require_send accepts only Send futures, which lets the example show the error without spawning anything.
use std::sync::Mutex;
fn require_send<F: std::future::Future + Send>(_future: F) {}
async fn pause() {}
async fn update(counter: &Mutex<u32>) {
let mut guard = counter.lock().unwrap();
*guard += 1;
pause().await; // the guard is still alive here
*guard += 1;
}
fn main() {
let counter = Mutex::new(0);
require_send(update(&counter));
}error: future cannot be sent between threads safely
--> src/main.rs:16:18
|
16 | require_send(update(&counter));
| ^^^^^^^^^^^^^^^^ future returned by `update` is not `Send`
|
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `std::sync::MutexGuard<'_, u32>`
note: future is not `Send` as this value is used across an await
--> src/main.rs:10:13
|
8 | let mut guard = counter.lock().unwrap();
| --------- has type `std::sync::MutexGuard<'_, u32>` which is not `Send`
9 | *guard += 1;
10 | pause().await; // the guard is still alive here
| ^^^^^ await occurs here, with `mut guard` maybe used later
note: required by a bound in `require_send`
--> src/main.rs:3:42
|
3 | fn require_send<F: std::future::Future + Send>(_future: F) {}
| ^^^^ required by this bound in `require_send`The error says the future returned by update "cannot be sent between threads safely" and that the MutexGuard is held across an await point. The fix is to end the guard's scope before the .await:
use std::sync::Mutex;
fn require_send<F: std::future::Future + Send>(_future: F) {}
async fn pause() {}
async fn update(counter: &Mutex<u32>) {
{
let mut guard = counter.lock().unwrap();
*guard += 1;
} // the guard is dropped here
pause().await;
*counter.lock().unwrap() += 1;
}
#[tokio::main]
async fn main() {
let counter = Mutex::new(0);
require_send(update(&counter));
update(&counter).await;
println!("counter = {}", counter.lock().unwrap());
}counter = 2Holding a std guard across an .await is also a deadlock risk on a single thread, because another task can be polled while the first one still owns the lock. Shared State: Mutex and RwLock covers the alternatives.
Example 14: Passing async work to a function
A function that takes a closure returning a future needs two type parameters: one for the closure and one for the future it returns. This is the usual shape of retry, timeout, and callback helpers.
use std::future::Future;
async fn apply_twice<F, Fut>(f: F, start: u32) -> u32
where
F: Fn(u32) -> Fut,
Fut: Future<Output = u32>,
{
let once = f(start).await;
f(once).await
}
async fn add_three(n: u32) -> u32 {
n + 3
}
#[tokio::main]
async fn main() {
println!("{}", apply_twice(add_three, 1).await);
println!("{}", apply_twice(|n| async move { n * 10 }, 2).await);
}7
200The closure |n| async move { n * 10 } returns an async block, which is a future. async move is needed because the block would otherwise borrow n, which belongs to the closure call.
Example 15: .await only works inside async
.await suspends the current future, so there has to be a current future. In an ordinary function the compiler reports E0728:
async fn value() -> u32 {
5
}
fn main() {
let n = value().await;
println!("{n}");
}error[E0728]: `await` is only allowed inside `async` functions and blocks
--> src/main.rs:6:21
|
5 | fn main() {
| --------- this is not `async`
6 | let n = value().await;
| ^^^^^ only allowed inside `async` functions and blocksThe fix is to make the caller async and give it a runtime, as #[tokio::main] does, or to call block_on from synchronous code (Your First Tokio Program).
Example 16: A future that is never awaited never runs
Because futures are lazy, forgetting .await drops the work. The compiler warns about an unused future through the lint unused_must_use, but the program still builds.
async fn save(record: u32) {
println!("saving record {record}");
}
#[tokio::main]
async fn main() {
let _ = save(1); // dropped without being polled
save(2).await;
println!("done");
}saving record 2
doneOnly record 2 is saved. The let _ = binding silences the warning. A bare save(1); on its own line compiles with the warning unused implementer of Future that must be used, followed by the note "futures do nothing unless you .await or poll them", and does the same thing at run time.
Interview points
- An
async fnreturns a future and does no work until the future is polled. Dropping an unpolled future discards the work. Future::pollreturnsPoll::Ready(value)orPoll::Pending. A future that returnsPendingmust have arranged for itsWakerto be called later..awaitpolls the inner future. If it isPending, the outer future returnsPendingand resumes at the same point on the next poll.- The compiler turns an
async fninto a state machine. Locals that live across an.awaitare stored in it, and they determine its size. - Sequential
.awaitcalls run one after another.join!and spawned tasks run several at once. - Different
async fntypes differ, soPin<Box<dyn Future<Output = T>>>is used to store or return several. Recursive async functions need a box. - A future is
Sendonly if everything held across an.awaitisSend. Astd::sync::MutexGuardheld across.awaitmakes it notSend. async movegives the future ownership of the variables it uses. A future that outlives its creator needs it.
Summary
async fnandasyncblocks produce futures. Futures are lazy.Futurehas one method,poll, that returnsReadyorPending..awaitdrives an inner future and passesPendingoutward.- A future is a state machine whose size depends on what it holds across
.await. join!polls several futures inside one task. Sequential.awaitdoes not overlap them.Box::pinandPin<Box<dyn Future>>unify future types and allow recursion.Sendfutures are required to move tasks between threads. End guard scopes before.await..awaitworks only in async contexts. A forgotten.awaitskips the work.
