#structured #concurrency #async #task

scoped_spawn

Full structured concurrency for asynchronous programming

4 releases

0.2.1 Jun 23, 2020
0.2.0 Jun 10, 2020
0.1.1 Feb 12, 2020
0.1.0 Feb 8, 2020

#112 in Asynchronous

Download history 4/week @ 2020-11-09 15/week @ 2020-11-16 14/week @ 2020-11-23 20/week @ 2020-11-30 2/week @ 2020-12-07 12/week @ 2020-12-14 67/week @ 2020-12-28 30/week @ 2021-01-04 1/week @ 2021-01-11 4/week @ 2021-01-18 6/week @ 2021-02-08 16/week @ 2021-02-15

394 downloads per month

MIT license

35KB
494 lines

Scoped Spawn

crates.io API Documentation Pipeline Status

Full structured concurrency for asynchronous programming.

Structured Concurrency

In structured concurrency, each asynchronous task only runs within a certain scope. This scope is created by the task's parent and cannot exceed the parent's scope.

At each point in time, tasks created within structured concurrency form a tree. If a node is alive, all the nodes on its path to the root are also alive. In other words, no node can outlive its parent.

This library provides a strong guarantee of structured concurrency: a child task must completely exit and release all resources (except for the resources to notify its parent of its termination, which is handled by this library) before we consider it terminated.

API Overview

This library provides the ScopedSpawn trait from which you can spawn new tasks. The spawned tasks become children of the current task and will be terminated when the current task begins to terminate. The API also provides methods with which you could terminate a child task earlier.

Termination initiated outside the task to be terminated is also called cancellation.

The ScopedSpawn trait is implemented by ScopedSpawner. To create a ScopedSpawner, pass it an object that implements Spawn, which you can trivially implement for all known executors.

Any code that wishes to accept a spawner should accept the ScopedSpawn trait instead of ScopedSpawner.

Termination of a Task

The termination process has several phases.

  1. Termination begins upon the completion of the task's future or the receipt of cancel signal from the parent, whichever comes first.
  2. The task's future is immediately dropped.
  3. The task in turn sends cancel signals to its children.
  4. The task asynchronously waits for its children to terminate.
  5. The done function is called in the task. For details see the documentation for ScopedSpawn.
  6. Finally, the task signals its termination to its parent through the "done" signal. For details see the documentation for ParentSignals and ChildSignals.

Low-level API

A low-level remote_scope API is also provided. It gives you everything you need to spawn a task but does not do the actual spawning.

Example

The following example demonstrates ScopedSpawn when using Tokio.

#[tokio::main]
async fn main() {
    use scoped_spawn::{ScopedSpawn, ScopedSpawner};

    let spawn = TokioDefaultSpawner::new();
    let spawn = ScopedSpawner::new(spawn);
    let signal = spawn
        .spawn_with_signal(
            |spawn| async {
                // Here `spawn` is the child's spawner. Do not give it to anyone else!
                // And do not try to use the parent's spawner because it would break structured
                // concurrency.

                // We could spawn nested children here, but for the demo we don't.
                drop(spawn);

                eprintln!("I'm alive!");
                tokio::time::delay_for(std::time::Duration::from_secs(2)).await;
                eprintln!("I'm still alive!"); // Nope.
            },
            || (),
        )
        .unwrap();

    tokio::time::delay_for(std::time::Duration::from_secs(1)).await;

    drop(signal.cancel_sender); // Cancel the task by dropping.
    signal.done_receiver.await; // Do this if you want to wait.
                                // When the await returns, the future of the spawned task is
                                // guaranteed dropped.
    eprintln!("Task terminated.");
}

// Just some chores to turn the Tokio spawner into a `Spawn`.
#[derive(Clone)]
struct TokioDefaultSpawner {}

impl TokioDefaultSpawner {
    fn new() -> Self {
        Self {}
    }
}

impl futures::task::Spawn for TokioDefaultSpawner {
    fn spawn_obj(
        &self,
        future: futures::future::FutureObj<'static, ()>,
    ) -> Result<(), futures::task::SpawnError> {
        tokio::spawn(future);
        Ok(())
    }

    fn status(&self) -> Result<(), futures::task::SpawnError> {
        Ok(())
    }
}

Dependencies

~1MB
~16K SLoC