A thread-pool executor for async Rust that runs the same code on OS threads natively and on web workers on wasm32.

This crate is the reference executor for the some_executor framework: if you write
code against the SomeExecutor trait and want one real executor to run it
everywhere, this is it.
Why this crate exists
Rust ships no async executor in std, and no trait for executors to implement, so most
async code is written directly against one specific runtime. The some_executor
crate fixes this by defining a small interface between the code that has futures and
the code that runs them: libraries spawn onto "some" executor through a trait, and
applications decide which executor that actually is.
That framework needs a production-quality executor to point at, and this crate is it.
some_global_executor implements the SomeExecutor trait for Send + 'static
futures with a resizable pool of workers, and can install itself as the process-wide
global executor or as a per-thread executor — which is exactly what
some_executor's current_executor() discovery hierarchy looks for. Out of the
box, some_executor falls back to a built-in toy executor that warns when used;
installing this crate is the intended way to replace it.
What it does
- One API, two platforms. On native targets, tasks run on a pool of OS threads fed
by a
crossbeam-channelqueue. On wasm32, the same public API runs tasks on web workers, with a custom async channel for distribution (workers can't block). The platform switch is conditional compilation; your code doesn't change. - Task observation. Spawning returns an observer from the
some_executorframework: await it for the result, poll it, detach it, or drop it to request cooperative cancellation. - Graceful shutdown.
Executor::drainblocks until every spawned task has finished;Executor::drain_asyncis the awaitable version for async contexts. - Runtime resizing.
Executor::resizegrows or shrinks the worker pool while the executor is running. - Structured logging. Internal operations are logged through
logwise, with optional diagnostic, forensic, and performance feature flags. An optionalexfiltratefeature exposes a registry of live pools for external debugging tools.
Quick start
use some_global_executor::Executor;
use some_executor::SomeExecutor;
use some_executor::task::{Task, Configuration};
# if cfg!(target_arch = "wasm32") { return; }
// A named pool with 4 worker threads (or web workers on wasm32).
let mut executor = Executor::new("my-executor".to_string(), 4);
let task = Task::without_notifications(
"example-task".to_string(),
Configuration::default(),
async { 42 }
);
let observer = executor.spawn(task);
// Await the observer for the result, or drop it to cancel the task.
// Optionally make this the executor that some_executor's
// current_executor() / global spawning resolves to:
executor.set_as_global_executor();
// Block until all spawned tasks finish.
executor.drain();
Where it fits in the ecosystem
The some_executor family divides the work like this:
some_executor— the trait layer. DefinesTask, observers, cancellation, priorities, task-locals, and the global/thread-local executor registry. No real scheduling of its own beyond a fallback.some_global_executor(this crate) — the reference executor forSendtasks: a thread pool natively, web workers on wasm32.some_local_executor— runs non-Sendtasks on the current thread, and can receive tasks from other threads.test_executors— toy executors for unit tests.
So: libraries depend on some_executor and stay executor-agnostic; applications
depend on this crate (or another implementation) and install it once at startup.
Alternatives
If you aren't committed to the some_executor interface, the field looks like this:
| Crate | What it is | Trade-off vs. this crate |
|---|---|---|
| tokio | Full runtime: executor plus async I/O, timers, sync primitives, and a huge ecosystem | The default choice if you need its I/O stack — but your code becomes tokio-specific, and there is no multithreaded wasm32 story. This crate is executor-only and trait-first. |
| async-executor (smol) | Small, composable executor; bring your own reactor | Closest in spirit (executor without a bundled runtime), but no common spawning trait for libraries, no observer/cancellation model, and no web-worker pool on wasm32. |
| futures-executor | The minimal ThreadPool/LocalPool in the futures crate |
Fine for gluing a future into sync code; no task metadata, draining, resizing, or wasm parallelism. |
| wasm-bindgen-futures | spawn_local onto the browser event loop |
The standard wasm answer, but single-threaded and wasm-only. This crate gives you actual parallelism via web workers and the same API natively. |
| rayon | Data-parallelism for synchronous code | Not an async executor at all; complementary rather than competing. |
| async-std | Formerly a full alternative runtime | Discontinued (deprecated in 2025); its maintainers point to smol. |
some_executor's built-in fallback |
Zero-config executor included in the trait crate | Always available so libraries work with no setup, but deliberately not production quality — it exists to be replaced by this crate. |
Pick this crate when you want executor-agnostic code (or already have it via
some_executor), need the same spawning code to work natively and on wasm32 with
real parallelism, and don't need a bundled I/O reactor. Pick tokio when you're
building on its networking stack and wasm isn't a target.
More examples
Observing task progress
Tasks can be observed to monitor their execution state:
use some_global_executor::Executor;
use some_executor::SomeExecutor;
use some_executor::task::{Task, Configuration};
use some_executor::observer::{Observer, Observation};
# if cfg!(target_arch = "wasm32") { return; }
let mut executor = Executor::new("observer-example".to_string(), 2);
let task = Task::without_notifications(
"monitored-task".to_string(),
Configuration::default(),
async { "result" }
);
let observer = executor.spawn(task);
// Poll the observer to check task state
loop {
match observer.observe() {
Observation::Ready(value) => {
println!("Task completed with: {}", value);
break;
}
Observation::Pending => {
// Task still running
std::thread::yield_now();
}
_ => break,
}
}
executor.drain();
Global executor pattern
Set an executor as the global default for the application:
use some_global_executor::Executor;
# if cfg!(target_arch = "wasm32") { return; }
// Create and configure the global executor
let executor = Executor::new("global".to_string(), num_cpus::get());
executor.set_as_global_executor();
// Now tasks can be spawned using the global executor from anywhere
// in the application without passing executor references
# executor.drain();
Dynamic thread pool management
Adjust executor capacity based on workload:
use some_global_executor::Executor;
let mut executor = Executor::new("dynamic".to_string(), 2);
// Scale up for heavy workload
executor.resize(8);
// Scale down during idle periods
executor.resize(2);
executor.drain();
Performance considerations
- Thread pool sizing: Default to
num_cpus::get()for CPU-bound work - For I/O-bound tasks, consider using more threads than CPU cores
- WASM targets have platform-specific limitations on parallelism
- Use
drain_async()in async contexts to avoid blocking
Logging
This crate uses the logwise framework for structured logging. Internal operations
are logged at various levels for debugging and monitoring:
# use some_global_executor::Executor;
// Executor creation and operations are automatically logged
let executor = Executor::new("logged-executor".to_string(), 4);
// Logs: "Creating executor with name logged-executor and 4 threads"
# executor.drain();
Requirements
Rust 1.95+ (edition 2024). Native targets need only stable Rust; running the wasm32 test suite requires nightly.