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:

  • JoinHandle has been renamed to TaskHandle since tasks under this crate’s model are lazy, not eager
  • TaskHandles are marked with #[must_use] to ensure they’re awaited
  • Tasks won’t start until their handle is .awaited
  • 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 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 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.



  • Spawn a task on the tokio multi-threaded executor using “strict” semantics.
  • Spawn a task on the tokio multi-threaded executor using “relaxed” semantics.