FuturesUnordered: An Efficient Way to Manage Multiple Futures in Rust
四月 17, 2025 [C/C++] #Rust #obsidian #编译链接FuturesUnordered: An Efficient Way to Manage Multiple Futures in Rust
FuturesUnordered is the life of the async party.
In this post, we’re going to explore futures::stream::FuturesUnordered, a mighty and efficient tool for juggling multiple asynchronous tasks concurrently. This utility allows us to poll multiple futures in a non-blocking manner, automatically handling the completion of individual tasks and yielding their results as they become available.
Let’s create a few asynchronous functions that simulate different tasks and use FuturesUnordered to run them concurrently. First, add the required dependencies to your Cargo.toml file:
[dependencies]
tokio = { version = "1.0", features = ["full"] }
futures = "0.3"
Next, create the asynchronous functions:
use std::time::Duration;
use tokio::time::sleep;// Define the async functions that return a String.
async fn task_one() -> String {
sleep(Duration::from_secs(3)).await;
"Task one completed".to_owned()
}
async fn task_two() -> String {
sleep(Duration::from_secs(1)).await;
"Task two completed".to_owned()
}
async fn task_three() -> String {
sleep(Duration::from_secs(2)).await;
"Task three completed".to_owned()
}
We defined three asynchronous functions task_one, task_two, and task_three, each of which simulates a task with varying durations using tokio::time::sleep. Now, we'll use FuturesUnordered to run these tasks concurrently:
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use std::pin::Pin;
use std::future::Future;#[tokio::main]
async fn main() {
// Create a FuturesUnordered struct with the same type for all futures: Box<dyn Future<Output = String>>.
let mut tasks = FuturesUnordered::<Pin<Box<dyn Future<Output = String>>>>::new();
// Use Box::pin to create a pinned, heap-allocated version of each future.
// This ensures that the memory location of the future does not change once it starts executing.
tasks.push(Box::pin(task_one()));
tasks.push(Box::pin(task_two()));
tasks.push(Box::pin(task_three()));
while let Some(result) = tasks.next().await {
println!("{}", result);
}
}
When you run this example, you’ll see that the tasks are executed concurrently, and their results are printed as they complete. The output may vary in order, as it depends on the completion times of each task:
Task two completed
Task three completed
Task one completed
In this example, we defined three asynchronous functions task_one, task_two, and task_three, each of which simulates a task with varying durations using tokio::time::sleep.
We created a FuturesUnordered instance called tasks and pushed our three tasks into it.
How does it work?
The theory behind futures::stream::FuturesUnordered lies in its implementation, which combines efficient task management and Rust's asynchronous programming model. Here's an overview of how it works under the hood:
FuturesUnorderedis a data structure that holds a set of futures. Internally, it maintains two linked lists of futures — one for futures that are ready to be polled and another for futures that are not yet ready.- When you add a future to
FuturesUnorderedusingpush(), it is initially placed in the list of not-ready futures. The future is then registered with an async runtime to be notified when it's ready to make progress (i.e. when it's ready to be polled). - When a future becomes ready, it’s moved from the not-ready list to the ready list. This happens as a result of the waker associated with the future being notified by the async runtime.
- The
FuturesUnorderedstruct itself implements theStreamtrait. When you callnext()on aFuturesUnorderedinstance, it polls the futures in the ready list. - The
next().awaitcall will resolve to a completed future from the ready list when one is available. If there are no ready futures, it will asynchronously wait until a future becomes ready. - When a future is polled and completed, it’s removed from the ready list, and the result is returned by
next().await. If the future is not completed, it remains on the ready list, and another future may be polled in the meantime. This process ensures thatFuturesUnorderedefficiently handles multiple futures concurrently, leveraging the available resources and avoiding blocking. - If a future is canceled or dropped, it will be removed from its respective list (ready or not ready), ensuring that any resources associated with the future are freed.
- The
FuturesUnorderedstream continues to return values as long as there are futures in the ready or not-ready lists. Once all futures have been completed or canceled, theFuturesUnorderedstream will returnNone, indicating that the stream has ended.
End
FuturesUnordered is the life of the async party. It makes sure no future is left behind, and every future gets its moment in the spotlight when the time is right.
We’ve seen how FuturesUnordered maintains two lists, one for the futures that are ready to be polled, and another for those still waiting for their time.
Utilizing the capabilities of FuturesUnordered, Rust applications can achieve improved performance, effectively manage concurrency, and maximize resource utilization. It is essential to acknowledge the significance of this remarkable tool and recognize that asynchronous programming in Rust can be both efficient and rewarding.
Happy coding!