Crate structured_spawn
source ·Expand description
Structured async task spawn implementations for Tokio
Read “Tree-Structured
Concurrency” for
a complete overview of what structured concurrency is, and how the popular
implementations of task::spawn fail to implement it. This crate provides
two flavors of structured spawn implementations for the Tokio async runtime:
- spawn_relaxed: this implementation guarantees error propagation and cancellation propagation. But it does not guarantee ordering of operations.
- spawn: this implementation guarantees error propagation, cancellation propagation, and ordering of operations. But it will block on drop to provide those guarantees. This may deadlock on single-threaded systems.
In the majority of cases spawn should be preferred over spawn_relaxed,
unless you’re able to provide other ways of mitigating ordering issues, or
you know for a fact that logical ordering of drop won’t matter. In those
case you should document those invariants and preferably test them too.
In the future we hope that async Rust will gain the ability to implement
some form of “async Drop”, which would resolve the tension between waiting
for destructors to complete asynchronously and not wanting to block inside
drop.
Differences with tokio::spawn
In Rust Futures are structured: if they’re fallible they will always
propoagate their errors, when dropped they will always propagate
cancellation, and they will not return before all the work of their
sub-futures has completed. Tasks on the other hand are not structured, since
they’ve been modeled after std::thread rather than std::future. The
solution we adopt in this crate is to instead of treating tasks as:
“async/.await versions of threads” we instead treat them as:
“parallelizable versions of futures”. That means making the following
changes:
JoinHandlehas been renamed toTaskHandlesince tasks under this crate’s model are lazy, not eagerTaskHandles are marked with#[must_use]to ensure they’re awaited- Tasks won’t start until their handle is
.awaited - When a
TaskHandleis dropped, the underlying task is cancelled - Cancelling a task will by default block until the cancellation of the task has completed
- In order for tasks to execute concurrently, you have to concurrently await the handles
- Because the relationship between handles and their tasks is now guaranteed, tasks will no longer produce cancellation errors
Implementing Concurrency
Because in this crate tasks behave like futures rather than threads, you
have to poll the TaskHandles concurrently in order to make progress on
them - just like you would with regular futures. For this we recommend using
the futures-concurrency library where possible, and futures-util
where not. futures-concurrency provides composable async concurrency
operations such as join, race, and merge. As well as fallible versions
of those operations, and operations such as zip and chain. Here’s an
example of concurrently awaiting multiple async TaskHandles:
use structured_spawn::spawn;
use futures_concurrency::prelude::*;
let mut handles = vec![];
for n in 0..100 {
handles.push(spawn(async move { n * n })); // 👈 Each task squares a number
}
let mut outputs: Vec<_> = handles.join().await; // 👈 The tasks start executing hereThe futures-concurrency library does not yet implement concurrency
operations for variable numbers of futures or streams. This is something
we’re actively exploring and will eventually be adding. For now it’s
recommended to instead use APIs such as FuturesUnordered from the
futures-util library instead.
Structs
- A handle which references a task.
Functions
- Spawn a task on the tokio multi-threaded executor using “strict” semantics.
- Spawn a task on the tokio multi-threaded executor using “relaxed” semantics.