| Crates.io | scoped-join-set |
| lib.rs | scoped-join-set |
| version | 0.8.3 |
| created_at | 2025-08-17 13:21:00.810277+00 |
| updated_at | 2026-01-06 21:28:54.458708+00 |
| description | A scoped variant of tokio::task::JoinSet for spawning non-'static futures. |
| homepage | |
| repository | https://github.com/Le-Maz/scoped-join-set |
| max_upload_size | |
| id | 1799403 |
| size | 42,376 |
A lifetime-aware, scoped concurrency wrapper for tokio::task::JoinSet.
This crate enables spawning non-'static futures that can borrow data from the stack, allowing for safe, efficient, and scoped parallel execution without the need for Arc, Mutex, or cloning.
It provides a functional scope API (similar to std::thread::scope) that ensures all tasks complete before the scope exits.
The scope function provided by this crate is not cancellation safe.
If the future returned by scope(...) is dropped before it completes (for example, if wrapped in a tokio::time::timeout or tokio::select! branch that gets cancelled), the process will abort immediately.
This strict behavior is required for soundness. Tasks spawned within the scope may borrow local variables from the surrounding stack. If the scope were dropped implicitly without waiting for tasks to finish, those tasks could continue running and access invalid memory (use-after-free). The library enforces an abort to prevent this undefined behavior.
'static futures: Tasks can borrow local variables directly from the stack.scope API: A high-level, easy-to-use closure-based API.Arc required: Avoids the overhead and complexity of reference counting for local data.The primary entry point is the scope function. It ensures that all spawned tasks are joined before the scope exits.
use scoped_join_set::scope;
#[tokio::main]
async fn main() {
let inputs = vec![1, 2, 3, 4, 5];
let multiplier = 10;
// We can borrow 'inputs' and 'multiplier' inside the scope
// without cloning or wrapping them in Arc.
let sum_result = scope(|scope_handle| async move {
for input_item in &inputs {
scope_handle.spawn(async move {
// Borrowing 'multiplier' from the stack
input_item * multiplier
});
}
let mut total = 0;
// Join tasks as they complete
while let Some(result) = scope_handle.join_next().await {
match result {
Ok(value) => total += value,
Err(e) => eprintln!("Task failed: {}", e),
}
}
total
}).await;
assert_eq!(sum_result, 150);
}
Tokio requires all spawned tasks to be 'static. To bypass this limitation safely, scope employs the following mechanism:
SendPtr) to the result slot.'static. This is sound because the scope guarantees the environment outlives the task execution.AbortOnDrop guard is active during the scope execution. If the scope future is dropped (cancelled) while tasks are running, the guard triggers std::process::abort() to prevent invalid memory access.JoinSet| Feature | Tokio JoinSet |
scoped-join-set |
|---|---|---|
Spawn non-'static tasks |
❌ No | ✅ Yes |
| Borrow from stack | ❌ No | ✅ Yes |
| Scoped lifetime | ❌ No | ✅ Yes |
| Drop Behavior | Safe (Detaches/Cancels) | Aborts Process if dropped early |
MIT OR Apache-2.0, at your option.