select!
Goal of This Episode
Learn to use select! to wait for “the first of several branches to finish with an output that matches its pattern,” and understand its close ties to cancellation.
Main Text
Waiting for “Whoever Arrives First”
join! waits for “all done.” select! is in a sense its opposite: it waits on several branches at once, and when one completes with an output that matches the pattern on the left, the handler for that branch runs and the whole select! ends — the other unfinished branches get dropped.
Basic Syntax
Each select! branch looks roughly like:
tokio::select! {
pattern = future => {
// when future completes, its output is caught by pattern
}
_ = other_future => {
// we don't care about other_future's output
}
}
The pattern before the equals sign catches the output of the Future after it; variables bound in the pattern are available inside the braces on the right. What goes after the equals sign is just the Future to wait on — do not add .await yourself. select! takes care of polling these Futures simultaneously, waiting for one to finish with an output that matches its pattern.
If you don’t need some Future’s output, ignore it with _, just like an ordinary match pattern:
tokio::select! {
value = compute() => {
println!("computed: {}", value);
}
_ = shutdown.recv() => {
println!("got the shutdown signal");
}
}
If the output is itself an Option<T> or Result<T, E>, the most intuitive style is to catch the whole value, then match it inside the handler:
tokio::select! {
message = receiver.recv() => {
match message {
Some(message) => println!("got a message: {}", message),
None => println!("the channel closed"),
}
}
_ = shutdown.recv() => {
println!("preparing to shut down");
}
}
select! can itself have a return value — the last expression in the winning branch’s braces. Much like match: every branch must return the same type.
let status = tokio::select! {
value = compute() => {
println!("computed: {}", value);
"done"
}
_ = shutdown.recv() => {
println!("got the shutdown signal");
"shutdown"
}
};
println!("status: {}", status);
The most classic use of select! is timeout: select! on “the real work” and “a timer” together and see which arrives first.
extern crate tokio;
use tokio::time::{sleep, Duration};
async fn do_work() {
sleep(Duration::from_secs(5)).await; // pretend the work takes five seconds
println!("work finished");
}
#[tokio::main]
async fn main() {
tokio::select! {
_ = do_work() => {
println!("the work completed fine");
}
_ = sleep(Duration::from_secs(1)) => {
println!("timeout! the work took too long — not waiting");
}
}
}
The timer fires at one second, beating the five-second job, so select! takes the timer branch, prints “timeout,” and drops the do_work() Future — the work is thereby cancelled.
select! shines in these situations:
- timeout (the example above).
- Receiving on multiple channels at once: whichever channel has a message first gets handled.
- Waiting for a shutdown signal: doing normal work while also listening for “time to wrap up,” responding to whichever comes first.
Watch Out for Cancellation Safety with select! in Loops
We just mentioned drop — this is exactly last episode’s cancellation: dropping a Future cancels it. And select!, by design, drops all the other branches when one wins. Grasping this keeps later select! usage out of the minefield.
select! is often placed inside a loop and run repeatedly (e.g. a server loop: each round select!s on “new work” or “the shutdown signal”). Such code demands special care about last episode’s cancellation safety.
Recall: operations like read_exact that “span multiple advances and accumulate state” are not cancellation safe — cancelled midway, some data may already be consumed with the “fill the buffer” operation unfinished. And every round of select! may drop (cancel) this branch’s Future because another branch finished first. Put a read_exact in a select! branch inside a loop, and it may well be cancelled mid-read, leaving a half-done state that’s hard to resume.
Losing branches never run their braces — that’s select!’s normal behavior and not the problem. The real thing to watch: before being discarded, the losing Future may already have produced external effects — bytes read off a socket, part of the data written out.
So the risk isn’t “the handler didn’t run”; it’s “the Future got cancelled with half-done work never properly wrapped up.” If the operation needs to accumulate progress across steps, keep the progress outside the select!, and let the branch wait only on a single safely cancellable small step. Later in this chapter we demonstrate designs following this principle.
A Few Practical Extras
select! has some further commonly used features:
Branch preconditions: append , if condition to a branch. The condition can use variables that already exist before entering the select!, such as accepting_jobs below, but it cannot use variables that will be bound by the pattern on the left. job becomes available inside the handler only after jobs.recv() completes and Some(job) matches successfully.
tokio::select! {
Some(job) = jobs.recv(), if accepting_jobs => {
handle(job).await;
}
_ = shutdown.recv() => {
accepting_jobs = false;
}
}
The relevant steps in one call to select! occur in this order:
- Evaluate every branch’s
ifprecondition. A branch whose condition is false is disabled for this call toselect!. - Evaluate every
asyncexpression on the right of an equals sign, including expressions in disabled branches. When a condition is false, the expression is still evaluated to create aFuture, but thatFutureis notpolled. Here, “evaluated” means creating theFuture, not running theasyncwork inside it; ordinary expressions such as argument calculations performed before creating theFuturestill run. polltheFutures of the remaining branches.- When a
Futurecompletes, try to match its output against the pattern on the left. If it matches, run the handler and finish theselect!; if it does not, disable that branch and continue waiting for the others. The first branch to complete is therefore not necessarily the winner; the winner is a branch that completes and whose pattern matches. - When all branches are disabled, run
else; if there is noelse,select!panics.
The else branch: for example, if Some(job) = jobs.recv() encounters a closed channel, .recv() returns None, so Some(job) fails to match and that branch is disabled. If every other branch is also disabled, the else branch runs.
tokio::select! {
Some(job) = jobs.recv(), if accepting_jobs => {
handle(job).await;
}
Some(msg) = messages.recv(), if accepting_messages => {
handle_message(msg).await;
}
else => {
break; // no branch could run this round
}
}
Fairness and biased;: by default, select! randomly chooses which branch to poll first. This matters mainly when select! runs repeatedly — typically in a loop — and multiple branches remain ready. Varying the starting branch reduces the risk that one branch wins every round simply because it appears earlier.
Adding biased; makes select! always poll from top to bottom. The branches are still polled one at a time, but select! stops as soon as one returns Ready and its output matches the pattern. An always-ready branch near the top can therefore prevent later branches from ever being polled:
loop {
tokio::select! {
biased;
// If messages are always waiting, this branch is always Ready.
Some(message) = messages.recv() => {
handle_message(message).await;
}
// This branch may never be polled, even after shutdown arrives.
_ = shutdown.recv() => {
break;
}
}
}
If messages.recv() is immediately ready on every iteration, it wins before shutdown.recv() is reached. This is starvation. With biased;, put a branch that must not be delayed first:
loop {
tokio::select! {
biased;
_ = shutdown.recv() => {
break;
}
Some(message) = messages.recv() => {
handle_message(message).await;
}
}
}
Recap
select!waits on several branches at once; the first branch to complete with an output matching its pattern runs its handler, and the rest getdropped (cancelled).- Basic syntax is
pattern = future => { ... }; don’t write.awaiton the left side of=>; use_ = futurewhen the output isn’t needed;select!can return the winning branch’s value. select!is thus the place in a program that manufactures the most cancellations; great for timeouts, multi-channel receives, and shutdown signals.- Using
select!in aloopdemands cancellation-safety care: keep non-cancellation-safeFutures likeread_exactout of branches that may bedropped. - Extras: branch
if(preconditions), branches disabled on pattern mismatch,else(when all branches are disabled), andbiased;for fixed top-down polling.