Rust Joining & Racing Futures
Joining futures means polling several futures together in one task. Each future makes progress while the others wait.
Waits in a row run one at a time
Each wait below runs to its end before the next one starts. Two short waits add up to one long wait:
use std::thread;
use std::time::Duration;
fn wait(name: &str) {
thread::sleep(Duration::from_millis(20));
println!("{name} done");
}
fn main() {
wait("first");
wait("second");
println!("both done");
}join! runs futures together
join! polls its futures side by side and collects the results as one tuple. Here the two helpers return Results, so the tuple holds both of them:
let pair = futures::join!(first(), second());
println!("pair = {pair:?}");try_join!: all-or-nothing joining
join! waits for everything. try_join! finishes as soon as the FIRST error appears. Sharp detail: the macro awaits internally, so there is no .await written after it.
let both = futures::try_join!(first(), second());
println!("both = {both:?}");The first error wins — and cancels the rest
Here broken() fails. So the whole expression is that error, immediately. The other future is dropped mid-flight. That is the drop-is-cancel rule from the cancellation-safety lesson.
let both = futures::try_join!(first(), broken());
println!("both = {both:?}");Same error type — or make it so
other() fails with a &str while first() fails with a String. map_err rewrites one future's error into the shared type so try_join! accepts both.
use futures::future::TryFutureExt; // for map_err on futures
let fixed = other().map_err(|e: &str| e.to_string());
let both = futures::try_join!(first(), fixed);
println!("both = {both:?}");FuturesUnordered: a bag of same-shaped futures
The number of futures is not always fixed at two or three. In that case, push them all into a FuturesUnordered. Drain it with next(). Results arrive in finish order — here only the sum matters, so the order cannot change the answer. Every pushed future must have the same type: return them all from one async fn, or box them.
use futures::stream::{FuturesUnordered, StreamExt}; // for push, next
let mut bag = FuturesUnordered::new();
bag.push(job(10));
bag.push(job(20));
let mut total = 0;
while let Some(n) = bag.next().await {
total += n;
}
println!("total = {total}");try_join! over spawned tasks
Inside an async fn that returns Result<_, JoinError>, two spawned tasks join fail-fast — one ? handles both a bad result and a dead task:
let a = tokio::spawn(async { 1 });
let b = tokio::spawn(async { 2 });
let (x, y) = tokio::try_join!(a, b)?;
println!("joined = {}", x + y);Example
// Cargo.toml: tokio = { version = "1", features = ["full"] }, futures = "0.3"
use futures::future::join_all;
use tokio::time::{sleep, Duration};
async fn job(name: &str, ms: u64) -> String {
sleep(Duration::from_millis(ms)).await;
format!("{} done", name)
}
#[tokio::main]
async fn main() {
let pair = tokio::join!(job("alpha", 10), job("beta", 20));
println!("pair = {} + {}", pair.0, pair.1);
let jobs = (1..=3).map(|n| async move { n * n });
let squares = join_all(jobs).await;
println!("squares = {:?}", squares);
tokio::select! {
winner = job("fast", 5) => println!("race winner = {}", winner),
_ = sleep(Duration::from_secs(5)) => println!("race timed out"),
}
}Key points
- A join polls each future in turn.
- So the futures make progress side by side.
- They do not run one after the other.
- The select function returns an Either.
- It returns Left(output) when the first future wins.
- It returns Right(output) when the second does.
- The winner is one or the other.
- It is named only by position.
- All futures in one join_all list must share one type.
- Futures of the same shape collect directly.
- Different async blocks have different hidden types.
- Pin each one as Pin<Box<dyn Future>>.
- Then they can be stored together.
- An unpinned Box<dyn Future> is rejected with error E0277.
- join_all is its partner for a collection the program builds as it runs.
- Racing picks the first future to finish.
In this example, job waits a few milliseconds and then reports its name. First, tokio::join! runs the alpha and beta jobs together. It waits for both. So the pair line shows both results. The waits overlap instead of adding up. Next, join_all runs a list of three small futures. A map built the list. There is one future for each number. join_all collects their squares into [1, 4, 9]. Finally, select! races a 5-millisecond job against a 5-second sleep. The job wins. Its line prints. The sleep future is dropped unfinished.
Note
the futures crate has its own select! macro too — unlike tokio::select!, it requires each future to be .fuse()d and stack-pinned with pin_mut! first, because it polls by reference so survivors can be selected again in a loop
join! is for futures you can name one by one in the code.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
You await future A (about 2 seconds), then await future B (about 2 seconds), one after the other in the same async block. How long until both are done?