๐ŸŽ“ A1Learner
๐Ÿ”

Rust Tokio Select

select! is a macro that waits on many futures at once. select! runs the branch of the future that finishes first.

The pain: waits in a line run one after another

Two waits written one after another run in order. The short wait cannot finish first, even though it needs less time:

sleep(Duration::from_millis(30)).await;
println!("long wait done");
sleep(Duration::from_millis(10)).await;
println!("short wait done");

Two waits, one winner

select! waits on several futures at the same time, and runs the branch of the one that finishes first:

tokio::select! {
    _ = sleep(Duration::from_millis(30)) => println!("the 30 ms timer won"),
    _ = sleep(Duration::from_millis(10)) => println!("the 10 ms timer won"),
}

Each branch: pattern = future => code

The future's result is matched by the pattern, then the branch's code runs. This shape โ€” the work in one branch, a sleep in the other โ€” is a timeout:

let job = async { 42 };
tokio::select! {
    answer = job => println!("answer = {answer}"),
    _ = sleep(Duration::from_secs(5)) => println!("timed out"),
}

In a loop, build the futures fresh

Each round of the loop creates its futures again โ€” a finished future must not be polled twice:

let (tx, mut rx) = mpsc::channel::<&str>(2);
tx.send("ping").await.unwrap();
drop(tx);
loop {
    tokio::select! {
        msg = rx.recv() => match msg {
            Some(text) => println!("got: {text}"),
            None => break,
        },
        _ = sleep(Duration::from_millis(50)) => { println!("idle too long"); break; }
    }
}

biased; checks branches in written order

Add biased; at the top when branch order is a priority. Both branches below are ready at once โ€” the first one written wins:

tokio::select! {
    biased;
    _ = std::future::ready(()) => println!("first branch wins"),
    _ = std::future::ready(()) => println!("second branch wins"),
}

Example

// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let fast_job = async { "job result" };

    tokio::select! {
        result = fast_job => println!("got: {}", result),
        _ = sleep(Duration::from_secs(5)) => println!("timed out"),
    }
}

Key points

  • select! runs the branch whose future finishes first.
  • The other futures are dropped, which cancels them.
  • A message and a timeout can race in one select!, and so can an answer and a cancel signal.

In this example, select! watches two futures at once: a job that produces a value straight away, and a five-second timer. The job is ready first, so its branch runs and prints the result. The timer future is dropped unfinished โ€” and that is exactly how timeouts are built.

Note

When several branches are ready at the same moment, select! picks one at random, so no branch can starve the others.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

In select!, what happens to the futures that lose the race?