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 Future
s 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:
JoinHandle
has been renamed toTaskHandle
since tasks under this crate’s model are lazy, not eagerTaskHandle
s are marked with#[must_use]
to ensure they’re awaited- Tasks won’t start until their handle is
.await
ed - When a
TaskHandle
is 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 TaskHandle
s 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 TaskHandle
s:
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 here
The 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.