Rust Futures
A future is a paused job that an executor can resume when its wait is over. Calling an async function builds a future and runs no code yet.
One thread for every wait
Each wait below gets a thread of its own. The thread spends its life sleeping. Thousands of waits would need thousands of threads:
use std::thread;
use std::time::Duration;
fn wait(name: &str) -> &str {
thread::sleep(Duration::from_millis(20));
name
}
fn main() {
let a = thread::spawn(|| wait("a"));
let b = thread::spawn(|| wait("b"));
let c = thread::spawn(|| wait("c"));
println!("{} done", a.join().unwrap());
println!("{} done", b.join().unwrap());
println!("{} done", c.join().unwrap());
println!("all waits done");
}Calling an async fn only builds the future
Futures are lazy: creating one starts nothing. An executor β a runtime, taught in the lessons ahead β is the worker that runs futures to the end:
async fn job() -> i32 { 42 }
fn main() {
let _future = job();
println!("future created; no work has happened");
}.await pauses this job, not the thread
While one job waits at .await, the same thread can run other jobs. That taking of turns is concurrency. Parallelism is different: several jobs run at the very same instant, on different cores. A multi-thread runtime can add that underneath:
let name = fetch_name().await; // pause here until the future finishes,
// then continue with its value β the thread stayed free meanwhileFutures are ordinary values
You can store them, pass them, and combine them, like any other value:
let jobs = vec![fetch(1), fetch(2), fetch(3)];
// three futures in a list β none of them running yetA task's handle is itself a future
A spawned task's JoinHandle is a future too:
let answer = handle.await;
// awaiting the handle waits for the task to finish
// and gives back its return valueWhat lives across an .await is stored inside the future
That is why futures have a size at all. It is why a fat local makes a fat future. And it is why holding a non-Send value (like an Rc) across an .await stops the task. The task then cannot run on a multi-threaded executor.
async fn report() {
let total = count_items().await; // total is saved inside the future
println!("{total}"); // while this line waits for its turn
}Example
// Needs a runtime (next lesson) in a real project:
// #[tokio::main] async fn main() { run().await; }
async fn fetch_name() -> String { "Ferris".to_string() }
async fn run() -> String { let name = fetch_name().await; format!("hello, {}", name) }
fn main() { println!("async fn returns a lazy Future; run() is driven by an executor"); println!("conceptual output: hello, Ferris"); }Key points
- Async suits I/O waits.
- Heavy computing belongs on threads (spawn_blocking, or the rayon crate).
- CPU-bound work is limited by the processor, like video encoding or one very large sum.
- I/O-bound work is limited by waiting for data, like network, file and timer waits.
- Async shines on I/O-bound work.
- IntoFuture: some types are not futures but turn into one when awaited β .await accepts them directly.
- Library builders use it so the builder itself can be awaited.
- Inside std, &mut F works the same way.
- Pin<P> works the same way, where the inside is a future.
- A future holds a job mid-wait, ready to run again when the wait ends.
- Calling an async fn and dropping the result does nothing at all.
- The compiler even warns you about the unused future.
- One thread can look after thousands of futures while they wait for replies, files, or timers.
In this example, fetch_name is an async fn that would produce a name. run awaits it and builds a greeting. But look at main: it only prints two lines explaining the idea. Nothing here actually runs run(), because there is no executor yet β and that is the point of the example. The output shows what an executor would produce: hello, Ferris. Tokio, the runtime that presses "go" on futures like these, arrives in a few lessons.
Note
This surprises newcomers from languages where calling starts the work.
Try it Yourself
Edit the code, then press Run.
Running is simulated Β· Rust Playground
Exercise
What happens when you call an async fn but never await it or spawn it?