The select! macro is designed to manage multiple asynchronous operations, allowing a program to wait on several tasks simultaneously and act on whichever completes first.
Understanding select!
The select! macro in Rust is used to wait for multiple asynchronous tasks or events and handle the one that completes first. This is particularly useful in scenarios where you need to respond to multiple sources of input or events, such as in network servers or user interfaces.
Basic Syntax
The basic syntax of the select! macro is as follows:
tokio::select! {
result1 = async_operation1() => {
// Handle the result of async_operation1
}
result2 = async_operation2() => {
// Handle the result of async_operation2
}
// Additional branches...
}
In this structure, each branch consists of a pattern (like result1 = async_operation1()), followed by a block of code to execute if that operation completes first.
A Simple Example
Let’s start with a basic example:
async fn task_one() -> String {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
"Task one completed".to_string()
}
async fn task_two() -> String {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
"Task two completed".to_string()
}
#[tokio::main]
async fn main() {
tokio::select! {
result = task_one() => {
println!("First task completed: {}", result);
}
result = task_two() => {
println!("Second task completed: {}", result);
}
}
}
In this example, task_two() will complete first because it has a shorter sleep duration. Therefore, the output will be "Second task completed: Task two completed".
Handling Cancellation
One of the key features of the select! macro is its ability to handle cancellation gracefully. When one branch completes, all other branches are dropped. This means if you have an asynchronous operation that is no longer needed because another operation completed, it will be canceled automatically.
Here’s an example:
async fn long_running_task() {
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
println!("Long running task completed");
}
async fn user_input() -> String {
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
"User input received".to_string()
}
#[tokio::main]
async fn main() {
tokio::select! {
_ = long_running_task() => {
println!("The long running task finished first.");
}
input = user_input() => {
println!("Received user input: {}", input);
// Other operations are cancelled here
}
}
}
In this case, the user_input function will likely complete first, and the long_running_task will be canceled.
Combining with futures::future::Either
For more complex scenarios where you need to know which future completed without writing separate handlers, you can combine select! with futures::future::Either. This can be especially useful when dealing with a large number of futures.
use futures::future::Either;
#[tokio::main]
async fn main() {
let result = tokio::select! {
result = task_one() => Either::Left(result),
result = task_two() => Either::Right(result),
};
match result {
Either::Left(val) => println!("Task one completed with: {}", val),
Either::Right(val) => println!("Task two completed with: {}", val),
}
}
Advanced Usage
Adding a Default Case
The select! macro also allows for a default case, which is executed if none of the other branches are ready:
tokio::select! {
result = async_operation() => {
// Handle the result
}
default => {
// This block executes if no other branches are ready
}
}


