Crate task_scope

Source
Expand description

A runtime extension for adding support for Structured Concurrency to existing runtimes.

§What is Strucutured Concurrency?

Structured Concurrency is a programming paradigm that lets asynchronous operations run only within certain scopes so that they form an operation stack like a regular function call stack. As the parent operation waits until all children complete, Structured Concurrency helps local reasoning of concurrent programs.

§Out-of-task concurrency considered harmful

Most of the asynchronous programs are composed of async functions and in-task concurrency primitives such as select and join, and this makes such futures automatically well-structured. As “futures do nothing unless polled,” executions of inner (child) operations are very explicit (usually at the point of awaits). Moreover, canceling a future is done by dropping it, which will reclaim resources used for the operation, including the inner futures. This drop-chain propagates the cancellation to the very end of the operation stack.

Out-of-task concurrencies such as spawn, however, breaks the structure. They allow us to start a new execution unit that can escape from the parent stack. Although frameworks provide a way to join on the spawned task, they don’t propagate cancellation properly. If you drop the spawning task, the spawned task may outlive the parent indefinitely.

task_scope is designed to offer a spawn function that properly respects cancellation. scope function delimits a lifetime within which inner tasks are allowed to run. If you issue a graceful cancellation or drop the scope, the runtime delivers a cancellation signal to all the child tasks.

§Cancellation points

task_scope requires the tasks to pass through cancellation points regularly to work effectively. Consider the following (contrived) example:

use tokio::io::*;

let mut read = repeat(42); // very fast input
let mut write = sink(); // very fast output

copy(&mut read, &mut write).await.unwrap();

This program is virtually running into an infinite loop because read and write never terminate. To make matters worse, this loop cannot be canceled from the outside because the I/O operations always succeed, and copy function tries to continue as much as possible. Therefore, the spawned task must cooperatively check for cancellation or yield the execution regularly in a loop to preserve the well-structuredness.

task_scope provides a convenience function cancelable to handle cancellation automatically. It wraps the given Future/AsyncRead/AsyncWrite and checks for cancellation (graceful or forced) before proceeding with the inner computation. The example above will look like:

use futures::pin_mut;
use tokio::io::*;

use task_scope::cancelable;

let read = cancelable(repeat(42)); // very fast, but cancelable input
pin_mut!(read); // needed for Unpin bound of copy
let mut write = sink(); // very fast output

// this will terminate with an error on cancellation
copy(&mut read, &mut write).await.unwrap();

If the cancellation logic is more complex, you can poll Cancellation manually to check for a cancellation signal.

§Grace period and mercy period

task_scope supports two modes of cancellation. Graceful cancellation and forced cancellation.

You can initiate a graceful cancellation by calling cancel method of the scope. This notifies and gives a “grace period” to tasks in the scope so that they can start their cancellation. The scope waits for all the tasks to complete as usual.

A forced cancellation is propagated to tasks when the scope is dropped, or force_cancel is called. Given that canceling a long-running is really hard as shown above, the canceled tasks are given to a “mercy period.” The tasks can continue their execution until they yield the execution next time, and then the runtime will automatically cancel the task. The tasks should shorten the mercy period as short as possible because it’s technically breaking the program’s concurrency structure (a child is outliving the dropped parent).

§task_scope is (almost) executor-agnostic

task_scope works by adding cancellation information to the current asynchronous context provided by runtimes such as async-std and Tokio. Therefore, the propagation of cancellation works regardless of executors. Even it’s possible to propagate a cancellation of a task running in a runtime to its child task running in another runtime.

However, task_scope doesn’t provide its own spawn API but delegates spawning to the executors. Thus, to use spawn, you need to declare which runtime(s) you want to extend with task_scope by enabling features of task_scope. For example, if you want to spawn a cancelable task in a Tokio runtime, you need to enable "tokio" feature of task_scope and to call task_scope::spawn::tokio::spawn. Currently, async-std and Tokio are supported.

If only one runtime is enabled, task_scope::spawn refers to the spawn function for the runtime. By default, only "tokio" feature is enabled.

Re-exports§

Modules§

Enums§

  • The error type for cancellation.