Use
JoinSetfor managing dynamic collections of spawned tasks
When spawning a variable number of tasks, collecting JoinHandles in a Vec and using join_all works but lacks flexibility. JoinSet provides a better abstraction: add/remove tasks dynamically, get results as they complete, and abort all on drop. It's the idiomatic way to manage task collections.
// Manual handle management
let mut handles: Vec<JoinHandle<Result<Data>>> = Vec::new();
for url in urls {
handles.push(tokio::spawn(fetch(url)));
}
// Wait for all, in order (not as they complete)
let results = futures::future::join_all(handles).await;
// No easy way to cancel all, handle errors progressively, or add more tasksuse tokio::task::JoinSet;
let mut set = JoinSet::new();
for url in urls {
set.spawn(fetch(url.clone()));
}
// Process results as they complete
while let Some(result) = set.join_next().await {
match result {
Ok(Ok(data)) => process(data),
Ok(Err(e)) => log::error!("Task failed: {}", e),
Err(e) => log::error!("Task panicked: {}", e),
}
}
// All tasks done, set is emptyuse tokio::task::JoinSet;
async fn worker_pool(mut rx: mpsc::Receiver<Task>) {
let mut set = JoinSet::new();
let max_concurrent = 10;
loop {
tokio::select! {
// Accept new tasks if under limit
Some(task) = rx.recv(), if set.len() < max_concurrent => {
set.spawn(process_task(task));
}
// Process completed tasks
Some(result) = set.join_next() => {
handle_result(result);
}
// Exit when no tasks and channel closed
else => break,
}
}
}use tokio::task::JoinSet;
{
let mut set = JoinSet::new();
set.spawn(long_running_task());
set.spawn(another_task());
// Early exit
return;
} // JoinSet dropped here - all tasks are aborted!
// Explicit abort
let mut set = JoinSet::new();
set.spawn(task());
set.abort_all(); // Cancel all tasksuse tokio::task::JoinSet;
async fn fetch_all(urls: &[String]) -> Vec<Result<Data, Error>> {
let mut set = JoinSet::new();
let mut results = Vec::new();
for url in urls {
set.spawn(fetch(url.clone()));
}
while let Some(join_result) = set.join_next().await {
let result = match join_result {
Ok(task_result) => task_result,
Err(join_error) => {
if join_error.is_panic() {
Err(Error::TaskPanicked)
} else {
Err(Error::TaskCancelled)
}
}
};
results.push(result);
}
results
}use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
async fn run_workers(shutdown: CancellationToken) {
let mut set = JoinSet::new();
for i in 0..4 {
let token = shutdown.child_token();
set.spawn(async move {
loop {
tokio::select! {
_ = token.cancelled() => break,
_ = do_work(i) => {}
}
}
});
}
// Wait for shutdown
shutdown.cancelled().await;
// Abort remaining tasks
set.abort_all();
// Wait for all to finish (drain aborted tasks)
while set.join_next().await.is_some() {}
}use tokio::task::JoinSet;
let mut set: JoinSet<(usize, Result<Data, Error>)> = JoinSet::new();
for (index, url) in urls.iter().enumerate() {
let url = url.clone();
set.spawn(async move {
(index, fetch(&url).await)
});
}
// Results include their index
while let Some(result) = set.join_next().await {
if let Ok((index, data)) = result {
results[index] = Some(data);
}
}| Feature | JoinSet | join_all |
|---|---|---|
| Add tasks dynamically | Yes | No |
| Results as-completed | Yes | No (all at once) |
| Abort all on drop | Yes | No |
| Cancel individual | Yes | No |
| Memory efficient | Yes | Pre-allocates |
- async-join-parallel - Static concurrent futures
- async-cancellation-token - Cancellation patterns
- async-try-join - Error handling in joins