Asynchronous Programming in Rust with join_all
Rust’s asynchronous capabilities are powerful, allowing developers to execute multiple tasks concurrently with ease. In this article, we’ll explore how to use the futures::future::join_all
function along with tokio
to manage multiple asynchronous tasks efficiently.

Understanding join_all
The join_all
function, provided by the futures
crate, enables us to execute multiple async tasks concurrently and collect their results. It is particularly useful when we have a set of independent tasks that need to run simultaneously without blocking one another.
join_all
is basically saying:
👉 “Everyone, do your async tasks, and I’ll wait until all of you are finished before we move on!”
Example: Running Multiple Async Tasks
Let’s take a look at an example where we perform five concurrent tasks and fetch data from a URL.
use futures::future::join_all;
use tokio::time::{sleep, Duration};
use reqwest;
async fn do_work(id: u32) -> u32 {
sleep(Duration::from_secs(1)).await;
println!("Task {} done!", id);
id * 2
}
async fn get_url(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let response = reqwest::get(url).await?;
let body = response.text().await?;
Ok(body)
}
#[tokio::main]
async fn main() {
let tasks = (1..=5).map(|i| do_work(i)).collect::<Vec<_>>();
let results = join_all(tasks).await;
println!("Results: {:?}", results);
let url = "https://example.com";
match get_url(url).await {
Ok(res) => println!("Response: {}", res),
Err(err) => println!("Error: {}", err),
}
}
Breaking Down the Code
do_work
Function: This function simulates a task that takes one second to complete and returns double its input value.get_url
Function: This function performs an HTTP GET request asynchronously and returns the body of the response.- Using
join_all
: We create five async tasks and usejoin_all
to execute them concurrently. - Fetching a URL: After completing the tasks, we make an HTTP request and handle any potential errors.
Benefits of join_all
- Concurrency: Tasks are executed simultaneously, maximizing efficiency.
- Scalability: Helps in handling multiple independent operations efficiently.
- Cleaner Code: Eliminates the need for manual loop-based async handling.
By leveraging join_all
and Rust’s async runtime, developers can write highly performant and scalable applications with minimal effort.
Yes, in a way! Using join_all
with async tasks is conceptually similar to manually managing spawned threads with Arc<Mutex<T>>
, but with key differences due to how async and threads work in Rust.
Similarities to Arc<Mutex<T>>
with Threads
- In multithreading, we often spawn threads and use
Arc<Mutex<T>>
to share data safely between them. - In async, we spawn tasks and use
join_all
to await all of them at once. - Both approaches allow independent operations to run in parallel without blocking each other.
Key Differences
Feature | join_all (async) | Arc<Mutex<T>> (Threads) |
---|---|---|
Execution Model | Cooperative async runtime (tokio , async-std ) | OS-managed preemptive threads |
Resource Usage | Lightweight tasks (non-blocking, uses a single thread pool) | Heavyweight, separate OS threads |
Synchronization | Futures resolve independently | Requires Arc<Mutex<T>> for shared state |
Overhead | Low, efficient context switching within runtime | Higher due to OS thread management |
When to Use Which?
- Use
join_all
(async) when your tasks are IO-bound (e.g., HTTP requests, DB queries, file reads). - Use threads (
Arc<Mutex<T>>
) when your tasks are CPU-bound (e.g., heavy computation, number crunching).
So, join_all
is like pushing handles into a vector when spawning threads, except it works within Rust’s async model instead of OS threads.
If we didn’t use join_all
, each async task would still start, but we wouldn’t properly await all of them together.
Scenario 1: Running Without join_all
(Bad Example)
#[tokio::main]
async fn main() {
for i in 1..=5 {
do_work(i).await; // Each task runs sequentially 😬
}
}
🔴 Problem:
- Each
do_work(i).await
runs one after another, not in parallel. - The total runtime would be 5 seconds (1 sec per task × 5 tasks).
Scenario 2: Using tokio::spawn
Instead (Needs Manual Handling)
use tokio::task;
#[tokio::main]
async fn main() {
let mut handles = vec![];
for i in 1..=5 {
handles.push(task::spawn(do_work(i))); // Spawns tasks, but we don't await them yet!
}
for handle in handles {
let result = handle.await.unwrap(); // Manually await each spawned task
println!("Result: {}", result);
}
}
🟡 This works, but:
- We need to manually collect and
.await
each spawned task. - Slightly more boilerplate than
join_all
.
Why join_all
is Better
#[tokio::main]
async fn main() {
let tasks = (1..=5).map(|i| do_work(i)).collect::<Vec<_>>();
let results = join_all(tasks).await; // Everything runs in parallel 🏃💨
println!("Results: {:?}", results);
}
✅ Advantage of join_all
:
- Runs all tasks concurrently (1 sec total, instead of 5).
- No need for extra
Vec
or.spawn()
. - Collects all results in one go.
So, without join_all
, you’d either be blocking sequentially or manually tracking tasks with spawn
. join_all
is the cleanest way to await all async operations together! 🚀
You’re right to question that! get_url()
isn’t actually involved in join_all
in your current example.
Why is get_url()
there?
It’s just another async function running after join_all()
. But it isn’t part of the batch of async tasks that join_all()
is handling.
If we wanted get_url()
to be inside join_all()
We’d modify the code like this:
#[tokio::main]
async fn main() {
let url = "https://example.com";
let tasks = (1..=5).map(|i| do_work(i)).collect::<Vec<_>>();
let fetch_task = get_url(url); // Add this as another async task
let all_tasks = join_all(tasks.into_iter().chain(std::iter::once(fetch_task))).await;
println!("All tasks completed: {:?}", all_tasks);
}
🟢 Now, get_url(url)
runs concurrently with do_work(i)
.
🚀 Everything in join_all()
runs together at the same time!
In Your Original Code
join_all(tasks).await;
runs alldo_work(i)
tasks in parallel.- Only after that finishes,
get_url(url).await;
is called separately.
If you want get_url()
to be part of the join_all()
, you need to include it in the task list!