tokio::select!
tokio::select! is a macro that waits on several
async operations at once and runs the branch of the first one that
completes.
Usecases of tokio::select!
1. Network read wait
2. process work from a channel or stop when Ctrl+C / shutdown
channel fires.
tokio::select! vs POSIX select() system call
Names sound similar but they live at different layers.
select() (POSIX syscall) |
tokio::select! (Rust macro) |
|
|---|---|---|
| Layer | OS kernel — watches file descriptors (sockets, pipes) | User space — watches async tasks / futures on Tokio runtime |
| Blocking? | Blocks the OS thread until a fd is ready (or timeout) | Non-blocking — task yields; thread can run other tasks |
| What you pass in | fd sets: readfds, writefds, … | Rust futures: rx.recv(), sleep(), … |
| Typical use | C servers: one thread, many client sockets | Async Rust: timeouts, shutdown, multiplexing channels |
| Relation |
Tokio’s reactor uses epoll / IOCP under
the hood (similar idea to select/poll, but faster)
|
Built on top of that reactor — you rarely call
select(2) yourself in Tokio code
|
Simple example — message or timeout
Wait for a value on a channel. If nothing arrives in 2 seconds, print “timeout” and exit.
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<String>(1);
// Another task sends a message after 3 seconds (too late)
tokio::spawn(async move {
sleep(Duration::from_secs(3)).await;
let _ = tx.send(String::from("hello")).await;
});
tokio::select! {
maybe_msg = rx.recv() => {
match maybe_msg {
Some(msg) => println!("got: {msg}"),
None => println!("channel closed"),
}
}
_ = sleep(Duration::from_secs(2)) => {
println!("timeout — no message in 2 sec");
}
}
}
// Output: timeout — no message in 2 sec
// (recv branch was cancelled when sleep finished first)