One of the big sources of difficulty on the async ecosystem is spawning tasks. Because there is no API in std for spawning tasks, library authors who want their library to spawn tasks have to depend on one of the multiple executors in the ecosystem to spawn a task, coupling the library to that executor in undesirable ways.

Ideally, many of these library authors would not need to spawn tasks at all. Indeed, I think probably most libraries which spawn tasks should be rewritten to do something else (and we should provide tools to make that easier, like async destructors).

Despite this, there’s a pretty clear desire for spawn sort of task spawning functionality in std. I just wanted to make some notes about what that could look like.

Spawn APIs: thread::block_on and task::spawn

I think there are two APIs that make sense to add to std eventually:

  • thread::block_on would be a function that takes a future and blocks the current thread until that future polls to completion.
  • task::spawn would be a function that takes a future and spawns it on some sort of “global executor,” running it to completion without blocking the current thread.

The first of these APIs has already been proposed. I think we need more discussion about the nuance of the API’s form, but overall its a good idea and relatively straightforward.

The second API is more complicated. Specifically we are forced to ask: what does it mean to add a “global executor” to Rust? I think global allocators provide a good model for how to move forward here.

#[global_executor]

What I would propose is a global executor attribute that functions just the same as the global allocator attribute we have already added to stable Rust. Only one of these attributes can be applied in given compilation, and so libraries should not use the attribute unless that is their express purpose (though they can use them in their tests and so forth, just outside of the normal library crate).

This attribute would be applied to some static that implements a GlobalExec trait or something of the sort, being the global executor for this compiled artefact.

Should there be a default global executor?

However, what if there is no executor entrypoint specified anywhere in this program? There are two obvious options:

  • task::spawn would panic by default.
  • We would ship a default executor to use in that case.

The latter case is nice, but would likely arouse controversy. We would need to be clear about what kinds of guarantees we make around the performance and behavior of our default global executor.

In particular, it’s possible to write a simple multithreaded executor in only a few hundred lines of Rust; I think we should not aim for something with better performance than that if we do this, and expect that most production users would replace the global executor with one more optimized for their kinds of workloads. However, it may be simpler just to provide nothing, but this is what the RFC process is for.

async fn main and #[test] async fn

Finally, having some kind of executor available as part of the libstd would enable us to make the main function and test functions into async contexts, setting up an async environment for you automatically. This way, you can .await directly inside of tests and the main function. Yoshua Wuyts talked about this a bit in his RustConf talk.

The big design question this raises is this: should these run the main future using the block_on executor or by spawning a task? Another question to figure out during the RFC period.

Next steps

Having laid out this road map, I think the easiest thing to move forward on is the block_on API, which has only minor design questions to resolve. I would love to see an RFC laying out a proposed design and its alternatives, so that we could begin to have this functionality on nightly.

We can also begin moving forward on the rest of these APIs - an RFC for the task::spawn and GlobalExec could also be appropriate. But we should be careful to follow those processes in a steady, inclusive and consensus-seeking manner and make sure we are “bringing the community along” with us.

Addendum: API sketch of task spawning in std

mod task {
    fn spawn<T, F>(future: F) -> JoinHandle<T> where
        F: Future<Output = T> + Send + 'static
    { ... }

    // TODO how do we allow GlobalExec impls to construct JoinHandle?
    struct JoinHandle<T> {
        async fn join(self) -> Result<T> { ... }
    }

    // The #[global_executor] static must implement this trait
    trait GlobalExec {
        fn spawn<T, F>(&self, future: F) -> JoinHandle<T> where
            F: Future<Output = T> + Send + 'static
        { ... }
    }
}