Async/Await is stabilizing in Rust 1.39. That means that the async ecosystem is growing rapidly. I would like to question how we deal with choosing executors when we need to spawn futures, especially in libraries that provide an async API.
I think we should allow library code to spawn futures without:
- bloating the dependency graph of client code with runtimes (reactors/timers/network dependencies, ...), and especially multiple versions of those if client code depends on several async libraries.
- deciding what executor implementation to use, and whether to spawn on a threadpool or on the current thread.
- limiting the use of the library to multithreaded systems (notably, WASM is currently single threaded) if the library does not otherwise require threads.
It's worth quickly touching on why should you spawn futures? Futures can be awaited or returned to the client code. While that is true, it doesn't always work out:
- you might have to bridge not async API's to async. It might be synchronous API's, callback based API's (Web API in WASM), you might be on a single threaded environment, ... and sometimes you just need something to run concurrently and it might be part of an implementation detail and not your API.
- you might not be in async context because you are implementing poll functions of Sinks, Streams, AsyncRead, ... In this context you cannot await, and you cannot return a future because the signature of these traits doesn't return futures.
So if we do need to spawn futures in library code, how can it be done?
Two possible designs come to mind when trying to solve the initial problem.
- library code that needs to spawn must always take in
T: Executor, but what is this Executor trait? It turns out the trait already exists, in a library almost all async code imports:
T: futures::task::Spawn. Oh, awesome. But, wait, none of the three main executor implementations we currently have: tokio, juliex and async-std implements this trait. Oops. Async-std doesn't even expose an executor object.
- We somehow make a global spawn function available everywhere. As an extra convenience, this does not clutter up the interfaces of your API. The downside is that it is not obvious that some piece of code spawns.
The poll below ask you whether the recommended way in Rust should be:
- executors should implement a
Spawntrait and libraries should take a generic executor object
- a library that provides a global spawn function that library code can call without pulling in any executor specific dependencies and leaving decisions to the app developer
Both approaches do not need to be exclusive. Even if all executors would implement
SpawnLocal where appropriate, we could still chose to provide API's that don't require passing around executors all over the place.
The question can be seen in the light of other similar questions. In OOP, it is considered good practice to require all your dependencies in the constructor. Makes it obvious up front. The downside is cluttering up all functions with a bunch of parameters because you have to pass around stuff all over the place.
At risk of influencing the poll I would still like to point out that Rust already has a similar example: logging. The
log crate makes logging available everywhere and lets app devs decide what implementation to use, where as
slog requires you to pass around a logger object, and as a benefit gives extra features like structured logging.
Which should be recommended?
- Pass around
E: SpawnLocal, or other traits yet to be created.
- Make a library that exposes a global spawn function and abstracts out over implementations?
- Both should exist and are equally valid
- I have another solution in mind (please leave a comment and enlighten us)