A runtime extension for adding support for Structured Concurrency to existing runtimes.
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.
Most of the asynchronous programs are composed of
async functions and in-task concurrency
primitives such as
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.
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
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
AsyncWrite and checks for cancellation
(graceful or forced) before proceeding with the inner computation. The example above will look
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.
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
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 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.
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
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.
The error type for cancellation.