Step 6 of 7
select!: the first one to finish wins
join! waits for all its futures. tokio::select! waits on several futures and runs the branch of whichever finishes first. The other futures are dropped, which cancels them on the spot.
use std::time::Duration;
use tokio::time::sleep;
async fn quick_answer() -> u32 {
42
}
#[tokio::main]
async fn main() {
tokio::select! {
n = quick_answer() => println!("answer arrived first: {n}"),
_ = sleep(Duration::from_secs(3)) => println!("gave up waiting"),
}
println!("the 3-second sleep was cancelled, not waited for");
}
answer arrived first: 42
the 3-second sleep was cancelled, not waited for
Each branch reads pattern = future => handler,. The handler runs with the pattern bound to that future's output. select! is an expression: if every handler returns a value of the same type, let x = tokio::select! { ... }; gives you the winner's value.
Timeouts
Racing a future against a sleep is so common that Tokio has a shortcut: tokio::time::timeout(duration, future) returns Ok(output) if the future finishes in time, or Err(Elapsed) if it doesn't (and then drops the future):
use std::time::Duration;
use tokio::time::{sleep, timeout};
async fn slow_report() -> String {
sleep(Duration::from_secs(3)).await;
String::from("report")
}
#[tokio::main]
async fn main() {
match timeout(Duration::from_millis(20), slow_report()).await {
Ok(report) => println!("got {report}"),
Err(_) => println!("report timed out after 20 ms"),
}
}
report timed out after 20 ms
Every call to another service in production code should have a timeout. Without one, a single stuck dependency can tie up your tasks forever.
select! in a loop
A long-running task usually loops over select! to handle whichever event comes next, for example new messages or a shutdown signal. When several branches are ready at the same moment, select! picks one at random (so no branch can starve the others). Add biased; as the first line to check the branches in the order written instead:
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(8);
let (stop_tx, mut stop_rx) = mpsc::channel::<()>(1);
for amount in [5, 10, 20] {
tx.send(amount).await.unwrap();
}
stop_tx.send(()).await.unwrap();
let mut total = 0;
loop {
tokio::select! {
biased;
Some(amount) = rx.recv() => total += amount,
_ = stop_rx.recv() => break,
}
}
println!("handled before shutdown: {total}");
}
handled before shutdown: 35
With biased;, waiting messages are always handled before the stop signal. Without it, the shutdown could win while messages are still waiting, and the total would change from run to run. break in a handler leaves the surrounding loop.
Cancellation
When a branch loses, its future is dropped in the middle of whatever it was doing: the code after its current .await never runs. Futures that survive this cleanly are called cancellation safe. rx.recv() and sleep are; a future that has read half a message from a socket is not. Tokio's documentation says which of its methods are cancellation safe, and it's worth checking before you put one in a select! loop.
Common mistakes
- Expecting the losing branch to finish later: it's dropped, not paused.
- Relying on branch order without
biased;: ready branches are picked at random. - No timeout on calls to other services: one hung dependency hangs your request.
Your task
Write async fn fetch_with_timeout(delay_ms: u64, limit_ms: u64) -> Result<String, String> with tokio::select!. Race fetch_quote(delay_ms) against a sleep of limit_ms milliseconds: return Ok(quote) if the quote comes first, or Err(format!("timed out after {limit_ms} ms")) if the sleep finishes first.
Challenge 2
Fill in Tokio's helper that gives up on a future after a time limit. The program should print backup timed out and then ping: pong.
Challenge 3
Three download mirrors answer at different speeds. Use tokio::select! with three branches to take whichever answers first, and print it:
fastest mirror: eu (3 ms)
The mirror function is given; call it with ("us", 300), ("eu", 3) and ("asia", 900).
Previous: Async channels: tokio::sync::mpsc Next: Async vs threads, spawn_blocking, and locks across .await