Crate async_scoped

source ·
Expand description

Enables controlled spawning of non-'static futures when using the async-std or tokio executors. Note that this idea is similar to crossbeam::scope, and rayon::scope but asynchronous.

§Motivation

Executors like async_std, tokio, etc. support spawning 'static futures onto a thread-pool. However, it is often useful to spawn futures that may not be 'static.

While the future combinators such as for_each_concurrent offer concurrency, they are bundled as a single Task structure by the executor, and hence are not driven in parallel.

§Scope API

We propose an API similar to crossbeam::scope to allow spawning futures that are not 'static. The key API is approximately:

pub unsafe fn scope<'a, T: Send + 'static,
             F: FnOnce(&mut TokioScope<'a, T>)>(f: F)
             -> impl Stream {
    // ...
}

See scope for the exact definition, and safety guidelines. The simplest and safest API is scope_and_block, used as follows:

async fn scoped_futures() {
    let not_copy = String::from("hello world!");
    let not_copy_ref = &not_copy;
    let (foo, outputs) = async_scoped::AsyncStdScope::scope_and_block(|s| {
        for _ in 0..10 {
            let proc = || async {
                assert_eq!(not_copy_ref, "hello world!");
                eprintln!("Hello world!")
            };
            s.spawn(proc());
        }
        42
    });
    assert_eq!(foo, 42);
    assert_eq!(outputs.len(), 10);
}

The scope_and_block function above blocks the current thread until all spawned futures are driven in order to guarantee safety.

We also provide an unsafe scope_and_collect, which is asynchronous, and does not block the current thread. However, the user should ensure that the returned future is not forgetten before being driven to completion.

§Executor Selection

Users may use “use-async-std”, or the “use-tokio” features to enable specific executor implementations. Those are not necessary, you may freely implement traits Spawner, Blocker, etc for your own runtime. Just ensure you follow the safety idea.

Some notes on default implementations:

  1. [AsyncScope] may run into a dead-lock if used in deep recursions (depth > #num-cores / 2).

  2. TokioScope::scope_and_block may only be used within a multi-threaded. An incompletely driven TokioScope also needs a multi-threaded context to be dropped.

§Cancellation

To support cancellation, Scope provides a spawn_cancellable which wraps a future to make it cancellable. When a Scope is dropped, (or if cancel method is invoked), all the cancellable futures are scheduled for cancellation. In the next poll of the futures, they are dropped and a default value (provided by a closure during spawn) is returned as the output of the future.

Note: this is an abrupt, hard cancellation. It also requires a reasonable behaviour: futures that do not return control to the executor cannot be cancelled once it has started.

§Safety Considerations

The scope API provided in this crate is unsafe as it is possible to forget the stream received from the API without driving it to completion. The only completely (without any additional assumptions) safe API is the scope_and_block function, which blocks the current thread until all spawned futures complete.

The scope_and_block may not be convenient in an asynchronous setting. In this case, the scope_and_collect API may be used. Care must be taken to ensure the returned future is not forgotten before being driven to completion.

Note that dropping this future will lead to it being driven to completion, while blocking the current thread to ensure safety. However, it is unsafe to forget this future before it is fully driven.

§Implementation

Our current implementation simply uses unsafe glue to transmute the lifetime, to actually spawn the futures in the executor. The original lifetime is recorded in the Scope. This allows the compiler to enforce the necessary lifetime requirements as long as this returned stream is not forgotten.

For soundness, we drive the stream to completion in the Drop impl. The current thread is blocked until the stream is fully driven.

Unfortunately, since the std::mem::forget method is allowed in safe Rust, the purely asynchronous API here is inherently unsafe.

Modules§

  • There you can find traits that are necessary for implementing executor. They are mostly unsafe, as we can’t ensure the executor allows intended manipulations without causing UB.

Structs§

  • A scope to allow controlled spawning of non ’static futures. Futures can be spawned using spawn or spawn_cancellable methods.

Type Aliases§