Rust made the terrible mistake of not having an async executor in std. And worse: there is no trait for executors to implement, nor a useful API for users to expect. The result is everyone has to write their code to a specific executor, and it's always tokio. But tokio has too many drawbacks to make it the universal choice, and the other executors are too cumbersome to be practical. As a result, async rust is stuck in limbo.
There are many proposals to fix this. This one's mine. Here's how it works:
If you want to execute futures, this crate provides a simple, obvious trait to spawn your future onto "some" executor. The rich APIs are thoughtfully designed to support typical applications, be fast, compatible with many executors, and future-proof. For example, you can take an executor as a generic argument, giving the compiler the opportunity to specialize your code for the specific executor. Or, you can spawn a task onto a global executor via dynamic dispatch. You can provide rich scheduling information that can be used by the executor to prioritize tasks. You can do all this in a modular and futureproof way.
Oh, and we built an executor into the crate. It's not the greatest, but it is a baseline that's always available.
These builtin executors print warnings when used to alert you they're not production-quality. If you want them to panic instead
(useful for catching missing executor configuration during development), set the environment variable SOME_EXECUTOR_BUILTIN_SHOULD_PANIC=1.
If you want to implement an executor, this crate provides a simple, obvious trait to receive futures and execute them, and plug into the ecosystem. Moreover, advanced features like cancellation are implemented for you, so you get them for free and can focus on the core logic of your executor.
If you want to write async code, this crate provides a standard, robust featureset that (in my opinion) is table-stakes for writing async rust in 2024. This includes cancellation, task locals, priorities, execution hints, and much more. These features are portable and dependable across any executor.
Here are deeper dives on each topic.
some_executor provides many different API options for many usecases.
- The
SomeExecutorExttrait provides an interface to spawn onto an executor. You can take it as a generic argument, specializing your code/types against the executor you want to use. - The
LocalExecutorExttrait provides the analogous interface for local executors (for your futures which are!Send). - The object-safe versions,
SomeExecutorandSomeLocalExecutor, are for when you want to store your executor in a struct by erasing their type. This has the usual tradeoffs around boxing types. - You can spawn onto the "current" executor, at task level
current_executoror thread levelthread_executor. This is useful in case you don't want to take an executor as an argument, but your caller probably has one, and you can borrow that. - You can spawn onto a program-wide
global_executor. This is useful in case you don't want to take it as an argument, you aren't sure what your caller is doing (for example you might be handling a signal), and you nonetheless want to spawn a task.
Spawning a task is as simple as calling spawn on any of the executor types. Then you get an TypedObserver object that you can use to get the results of the task, if interested, or cancel the task.
- test_executors provides a set of toy executors good enough for unit tests.
- some_local_executor provides a local executor that runs its task on the current thread, and can also receive tasks from other threads.
- A reference thread-per-core executor is planned.
Here are your APIs:
- Implement the
SomeExecutorExttrait. This supports a wide variety of callers and patterns. - Alternatively, or in addition, if your executor is local to a thread, implement the
LocalExecutorExttrait. This type can spawn futures that are!Send. - Optionally, respond to notifications by implementing the
ExecutorNotifiedtrait. This is optional, but can provide some efficiency. - For static executors (non-Send), use the
static_supportmodule to erase notifier types and create unified interfaces viaOwnedSomeStaticExecutorErasingNotifier.
The main gotcha of this API is that you must wait to poll tasks until after Task::poll_after. You can
accomplish this any way you like, such as suspending the task, sleeping, etc. For more details, see the documentation.
Mostly, write the code you want to write. But here are some benefits you can get from this crate:
- If you need to spawn
Tasks from your async code, see above. - The crate adds the
task_localmacro, which is comparable tothread_localor tokio's version. It provides a way to store data that is local to the task. - The crate provides various particular task locals, such as
task::TASK_IDandtask::TASK_LABEL, which are useful for debugging and logging information about the current task. - The crate propagates some locals, such as
task::TASK_PRIORITY, which can be used to provide useful downstream information about how the task is executing. - The crate provides the
task::IS_CANCELLEDlocal, which can be used to check if the task has been cancelled. This allows you to return early and avoid unnecessary work. - The crate provides
hint::Hintto communicate expected task behavior (I/O-bound vs CPU-bound) to executors, enabling better scheduling decisions. - In the future, support for task groups and parent-child cancellation may be added.
One way to understand this crate is as an alternative to the executor-trait project. While I like it a lot, here's why I made this instead:
- To support futures with output types that are not
(). - To avoid boxing futures in cases where it isn't really necessary.
- To provide hints and priorities to the executor.
- To support task locals and other features that are useful for async code.
- To support task cancellation much more robustly.
Philosophically, the difference is that executor-trait ships the lowest-common denominator API that all executors can support. While this
crate ships the highest-common denominator API that all async code can use, together with polyfills and fallbacks so all executors
can use them even if they don't support them natively. The result is rich, fast, easy, and portable async rust.
It is straightforward to implement the API of this crate in terms of executor-trait, as well as the reverse. So it is possible
to use both projects together.
This interface is unstable and may change.
This crate has full support for wasm32-unknown-unknown.
