Skip to main content

Crate some_executor

Crate some_executor 

Source
Expand description

§some_executor

logo

Rust made the terrible mistake of not having an async executor in std. Worse, there is no trait for executors to implement, and no API for async code to expect. So everyone writes their code against one specific executor, and it’s always tokio. But tokio has too many drawbacks to be the universal choice, and the other executors are too cumbersome to be practical. Async rust is stuck in limbo.

There are many proposals to fix this. This one’s mine.

some_executor is a small crate that sits between the code that has futures and the code that runs them:

  • If you spawn futures, you get one obvious spawn that works on “some” executor: a generic argument, a stored trait object, the executor your caller is already running on, or a program-wide global. Whichever you pick, you get back an observer you can await, poll, detach, or drop to cancel.
  • If you write an executor, you implement one trait, and cancellation, task-locals, observers, priorities and hints are done for you. You get to focus on scheduling.
  • If you write async code, you get the featureset that (in my opinion) is table stakes for async rust: cancellation, task-locals, priorities, execution hints, task IDs and labels. They work the same on every executor, because the executor doesn’t implement them; this crate does.

There is also a built-in fallback executor, so all of this works out of the box. It is not good, but it is always there; when you want a real one, see the reference executors below.

§Quick start

use some_executor::SomeExecutor;
use some_executor::current_executor::current_executor;
use some_executor::observer::FinishedObservation;
use some_executor::task::{Configuration, Task};

// A Task is a future plus a label and some scheduling metadata.
let task = Task::without_notifications(
    "add".to_string(),
    Configuration::default(),
    async { 2 + 2 },
);

// Spawn it on whatever executor is current: the one this task is running on,
// else the thread's, else the global one, else the built-in fallback.
let mut executor = current_executor();
let observer = executor.spawn(task);

// The observer is a Future.  Dropping it instead would cancel the task.
match observer.await {
    FinishedObservation::Ready(value) => assert_eq!(value, 4),
    FinishedObservation::Cancelled => unreachable!(),
}

The rest of this page is organized by what you are trying to do.

§Spawning tasks

Every path to spawning starts with a Task: your future, a String label, and a Configuration. Then pick how you want to get hold of an executor.

You want to…Use
Take an executor as a generic argument and monomorphizeSomeExecutorExt (or StaticExecutorExt, LocalExecutorExt)
Store an executor in a struct, erasing its typeDynExecutor, DynStaticExecutor, SomeLocalExecutor
Borrow the executor your caller is already running oncurrent_executor, or Task::spawn_current for fire-and-forget
Use the executor pinned to this threadthread_executor, thread_static_executor, thread_local_executor
Spawn from nowhere in particular (a signal handler, say)global_executor

Whichever you use, spawn returns an Observer (usually a TypedObserver):

  • .await it to get a FinishedObservation: the task’s output, or Cancelled.
  • Call observe to peek without waiting.
  • Call detach to let the task run to completion unobserved.
  • Drop it to request cancellation. Cancellation is cooperative: the task sees it through IS_CANCELLED, and executors may stop polling.

§Which executor is “current”?

current_executor walks a fixed hierarchy and always returns something:

  1. The executor the current task was spawned on (TASK_EXECUTOR).
  2. The executor set for this thread with set_thread_executor.
  3. The program-wide executor set with set_global_executor.
  4. The built-in fallback executor.

The fallback executor exists so that libraries built on this crate work with zero configuration. It prints a warning when used, because it is not production quality; install a real one (see reference executors) with set_global_executor or set_thread_executor. Set SOME_EXECUTOR_BUILTIN_SHOULD_PANIC=1 to make the fallback panic instead, which is a good way to find places you forgot to do that.

§Three flavors of executor

Executors differ in what futures they can accept. This crate models the three cases that come up in practice, each with a generic (*Ext) trait for static dispatch and an object-safe base trait for dynamic dispatch:

Futures are…Object-safe traitGeneric traitTypical executor
Send + 'staticSomeExecutor (see DynExecutor)SomeExecutorExtThread pools; anything that moves work
'static, not SendSomeStaticExecutor (see DynStaticExecutor)StaticExecutorExtMain-thread and single-threaded executors; wasm
Neither ('a and !Send)SomeLocalExecutorLocalExecutorExtExecutors scoped to a stack frame

The Send and static flavors are cloneable, like a channel sender, and can be discovered through current_executor and friends. Local executors are borrowed and cannot be cloned; the SomeLocalExecutor docs explain the lifetime parameter and why. If you don’t know which you need, start with SomeExecutor.

§Configuring a task

Configuration (build one with ConfigurationBuilder) carries three things an executor may use, and none of them changes what your future does:

  • A Hint: is this task expected to be I/O-bound, CPU-bound, or unknown?
  • A Priority, for executors that schedule by priority.
  • A poll_after Instant, before which the executor must not poll the task.

Every task also has a label and a TaskID, both visible from inside the task and from its observer, which makes tracing and logging across executors practical.

§Reference executors

These crates implement the traits above and are the ones I actually use:

  • some_global_executor is a thread-pool executor for Send tasks. It runs on OS threads natively and on web workers on wasm32, and can install itself as the thread or global executor. If you want one executor to replace the built-in fallback, this is it.
  • some_local_executor is a local executor that runs its tasks on the current thread and can also receive tasks from other threads.
  • test_executors provides toy executors good enough for unit tests.

§Crossing from sync into async

Every program crosses from sync into async exactly once: in fn main, in a test, in a CLI tool, at an FFI callback. Which API you want depends on whether you are choosing an executor or using one.

Choosing is ExecutorMain. It is the trait spelling of “construct the chosen backend, install it, run this future”, so that an #[some_executor::main(SomeBackend)] attribute can expand to something that compiles against a backend the macro has never heard of. It returns () rather than the future’s output, which is what makes it implementable on the wasm32 main thread as well as natively.

Using is SomeExecutor::block_on. Given an executor – including the one current_executor hands you – it drives a future to completion and returns its output, blocking the calling thread:

use some_executor::SomeExecutor;
use some_executor::current_executor::current_executor;

let mut executor = current_executor();
assert_eq!(executor.block_on(async { 2 + 2 }), 4);

Underneath both is the free block_on function, which polls a future in place on the calling thread with no executor involved. Because the future never leaves the thread, it needs neither Send nor 'static and may borrow from the stack – unlike spawning:

let name = String::from("world");
let greeting = some_executor::block_on(async { format!("hello {}", name.as_str()) });
assert_eq!(greeting, "hello world");

Blocking is not universally available, which is why the entry point and the primitive are separate. It works whenever the blocking thread can keep driving every scheduler the future depends on: another thread is doing the work, or the executor owns its own loop and runs it here. It cannot work when the wakeups come from a scheduler this thread can only run by unwinding – the browser main thread, where the JavaScript event loop delivers every timer, promise and worker message. block_on panics there with an explanation rather than hanging the tab; a wasm32 worker has no such problem, and ExecutorMain is the portable choice for an entry point.

§Writing async code

Mostly, write the code you want to write. Nothing here requires you to know which executor you are running on. What you get on top:

use some_executor::task_local;
use some_executor::task::{TASK_LABEL, IS_CANCELLED};

task_local! {
    static REQUEST_ID: u64;
}

async fn handle() {
    let label = TASK_LABEL.with(|l| l.cloned());
    let request = REQUEST_ID.get();
    // Do a unit of work, then check for cancellation before the next one.
    if IS_CANCELLED.with(|c| c.map(|c| c.is_cancelled()).unwrap_or(false)) {
        return;
    }
    let _ = (label, request);
}

§Implementing an executor

An executor is anything that accepts a Task and polls it. In outline:

  1. Implement SomeExecutor (for Send futures), SomeStaticExecutor (for 'static, !Send futures) and/or SomeLocalExecutor (for borrowed futures). Add the matching *Ext marker trait if your executor is Clone.
  2. In your spawn, call Task::spawn (or spawn_static / spawn_local) with &mut self. You get back a SpawnedTask to schedule and an observer to hand to the caller.
  3. Poll the spawned task. Its poll takes an executor context so that current_executor works inside the task; the spawned task itself installs task-locals, reports completion to the observer, and stops early on cancellation.
  4. Respect poll_after: do not poll before that instant. Sleep, defer, re-queue, whatever fits your design. This is the main gotcha.
  5. Optionally implement ExecutorNotified to be told when a task’s observer requests cancellation, so you can drop it early instead of discovering that on the next poll.
  6. Optionally register yourself with set_thread_executor / set_global_executor (or the static and local equivalents) so that current_executor finds you.
  7. If your executor runs tasks on the calling thread, override SomeExecutor::block_on_objsafe. The default parks the caller, which is correct for a thread pool and a deadlock for a current-thread executor; run your own polling loop until the future resolves instead. Implement ExecutorMain too, so #[some_executor::main] can name you.

For static executors, static_support provides OwnedSomeStaticExecutorErasingNotifier to erase your notifier type into the common DynStaticExecutor shape. For the object-safe methods (spawn_objsafe and friends), the ObjSafe* and Boxed* type aliases at the crate root spell out the erased types so you don’t have to.

§Compared with executor-trait

One way to understand this crate is as an alternative to executor-trait. I like it a lot; here is why I made this instead:

  1. To support futures whose output isn’t ().
  2. To avoid boxing futures where it isn’t necessary.
  3. To carry hints and priorities to the executor.
  4. To provide task-locals and the other features async code actually needs.
  5. To support cancellation much more robustly.

Philosophically, executor-trait ships the lowest common denominator that every executor can support. This crate ships the highest common denominator that all async code can use, together with polyfills and fallbacks so every executor can offer it, even ones that don’t support a feature natively. It is straightforward to implement either crate’s API in terms of the other, so the two can be used together.

§wasm32

wasm32-unknown-unknown is a first-class target, with and without atomics. Timing uses Instant, which is std::time::Instant natively and a web-clock on wasm, and the fallback executor schedules through the browser event loop.

§Status

This interface is unstable and may change.

Re-exports§

pub use block_on::block_on;
pub use entry_point::ExecutorMain;
pub use static_executor::SomeStaticExecutor;
pub use static_executor::StaticBlockOn;
pub use static_executor::StaticExecutorExt;

Modules§

block_on
Drives a future to completion from synchronous code.
context
Task-local storage for async tasks.
current_executor
Provides access to the current executor in various contexts.
entry_point
An executor-agnostic program entry point.
global_executor
Global executor management for program-wide task execution.
hint
Execution hints for guiding executor scheduling decisions.
observer
The channel between a spawned task and whoever spawned it.
static_executor
Executors for futures that are 'static but not Send.
static_support
Support for static executors, primarily for type erasure.
task
Task management and execution primitives for the some_executor framework.
thread_executor
Thread-local storage for executors.

Macros§

task_local
Declares task-local storage keys.

Structs§

Instant
Platform-appropriate instant type for time measurements.

Traits§

LocalBlockOn
Opt-in synchronous entry point for an executor that drives local tasks on the calling thread.
LocalExecutorExt
A non-objsafe descendant of SomeLocalExecutor.
SomeExecutor
A trait targeting ‘some’ executor.
SomeExecutorExt
A non-objsafe descendant of SomeExecutor.
SomeLocalExecutor
A trait for executors that can spawn tasks onto the local thread.

Type Aliases§

BoxedBlockOnFuture
Type alias for the type-erased future accepted by SomeExecutor::block_on_objsafe.
BoxedLocalFuture
Type alias for a boxed future that outputs boxed Any (non-Send).
BoxedLocalObserver
Type alias for a boxed observer that handles Any values (non-Send).
BoxedLocalObserverFuture
Type alias for a future that returns a boxed observer for local Any values.
BoxedLocalObserverNotifier
Type alias for a boxed observer notifier that handles Any values (non-Send).
BoxedSendFuture
Type alias for a boxed future that outputs boxed Any and is Send + ’static.
BoxedSendObserver
Type alias for a boxed observer that handles Send Any values.
BoxedSendObserverFuture
Type alias for a future that returns a boxed observer for Send Any values.
BoxedSendObserverNotifier
Type alias for a boxed observer notifier that handles Send Any values.
BoxedStaticFuture
Type alias for a boxed future that outputs boxed Any and is ’static but not Send.
BoxedStaticObserver
Type alias for a boxed observer that handles ’static Any values (non-Send).
BoxedStaticObserverFuture
Type alias for a future that returns a boxed observer for static Any values.
BoxedStaticObserverNotifier
Type alias for a boxed observer notifier that handles ’static Any values (non-Send).
DynExecutor
The appropriate type for a dynamically-dispatched executor.
DynStaticExecutor
The appropriate type for a dynamically-dispatched static executor.
ObjSafeLocalTask
Type alias for a Task that can be used with local object-safe spawning.
ObjSafeStaticTask
Type alias for a Task that can be used with static object-safe spawning.
ObjSafeTask
Type alias for a Task that can be used with object-safe spawning.
Priority
Task priority for scheduling hints.

Attribute Macros§

main
Turns an async fn main into a fn main that installs an executor and runs it. See entry_point for the trait it expands to a call on.