๐ŸŽ“ A1Learner
๐Ÿ”

Rust Select Patterns

select! races several futures and the first finisher wins. Real programs race inside loops โ€” every message, every tick, until a stop signal or a closed channel ends it.

A branch runs only when its pattern matches

The left side of a branch is a pattern. When the channel closes, recv() returns None, the pattern cannot match, and the else branch runs instead:

tokio::select! {
    Some(n) = rx.recv() => println!("got {n}"),
    else => break,
}

An if guard switches a branch off

A guard after the future switches its branch off for that round. The work branch is guarded by !paused, and paused is true โ€” so the idle branch runs:

let paused = true;
tokio::select! {
    _ = std::future::ready(()), if !paused => println!("work ran"),
    _ = std::future::ready(()) => println!("idle instead"),
}

biased; makes the first ready branch win

Both branches here are ready at once. Without the biased keyword, the winner would be a coin toss. With it, the stop branch โ€” written first โ€” wins every time:

tokio::select! {
    biased;
    _ = std::future::ready(()) => println!("winner: stop"),
    _ = std::future::ready(()) => println!("winner: work"),
}

Keep a long job across rounds

The job is built once and pinned outside the loop. Each round races the same job against a tick. The tick branch can fire many times โ€” the job keeps its progress between rounds:

let mut job = Box::pin(long_job());
loop {
    tokio::select! {
        done = &mut job => { println!("job finished"); break; }
        _ = tick.tick() => println!("still working..."),
    }
}

A futures-crate loop needs fused futures

Both futures are fused and pinned before the loop, so they survive from round to round. Each round takes whichever is still running. When both are spent, complete ends the loop:

let mut total = 0;
let fut1 = future::ready(1).fuse();
let fut2 = future::ready(2).fuse();
pin_mut!(fut1, fut2);
loop {
    select! {
        n = fut1 => total += n,
        n = fut2 => total += n,
        complete => break,
    }
}
println!("total = {total}");

Example

// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel::<i32>(4);

    tokio::spawn(async move {
        for n in 1..=3 {
            tx.send(n).await.unwrap();
        }
        // tx drops here: the channel closes
    });

    loop {
        tokio::select! {
            Some(n) = rx.recv() => println!("got {n}"),
            else => break,
        }
    }
    println!("channel closed, loop ended");
}

Key points

  • Futures written inside select! are rebuilt every round.
  • That is fine for cancel-safe one-shot calls like recv().
  • But a future carrying progress โ€” a half-read line, a multi-step job โ€” loses that progress every round.
  • Without the biased keyword, tokio::select! shuffles the branch order on purpose.
  • So no branch can starve the others.
  • Never rely on a fair select! preferring your favourite branch.
  • A select! loop races fresh futures on every round until a stop branch ends it.

In this example, a sender task posts 1, 2, 3 into a channel. It then drops its sender, which closes the channel. The receiving loop races one branch: recv() with the pattern Some(n). Three times, a value arrives, the pattern matches, and the value prints. The fourth time, recv() returns None โ€” the channel is closed and empty. The pattern Some(n) cannot match None, so the only branch is disabled, and the else branch runs: break. That is the standard ending of a select message loop โ€” no flag, no counter, just a pattern that stops matching.

Note

Say it with the biased keyword instead.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

A select! loop must always prefer the shutdown branch when shutdown and a message are ready at the same moment. What do you add?