Asynchronous Rust
Standard Library vs. External Async Runtimes
Rust intentionally separates the core asynchronous language abstractions (built into the compiler and core / std) from the asynchronous execution runtime (provided by ecosystem crates such as tokio, smol, embassy, and futures).
Why Rust Does Not Bundle a Single Monolithic Async Runtime in std
Different target environments have fundamentally incompatible requirements for an async runtime:
- High-throughput cloud servers (
tokio): Need a multi-threaded work-stealing scheduler,epoll/kqueue/IOCPnetwork reactors, and a blocking thread pool. - Bare-metal microcontrollers (
embassy): Have0OS threads,0heap allocator (#![no_std]), and wake tasks directly from hardware interrupt service routines (ISRs). - Linux
io_uringstorage engines (monoio,glommio): Use a thread-per-core shared-nothing architecture where futures are intentionally!Send. - WebAssembly in the browser (
wasm-bindgen-futures): Bridges RustFutures directly onto the browser's single-threaded JavaScriptPromisemicrotask queue.
By putting Future, Poll, Pin, Waker, and async/.await in core/std without mandating a single OS reactor, all of these environments share the exact same language syntax and trait interfaces!
Division of Responsibilities in Async Rust:
+-------------------------------------------------------------------+
| Rust Compiler (`rustc`) & Standard Library (`core` / `std`) |
| - `async fn`, `async {}`, `async ||`, `.await` syntax |
| - State-machine lowering |
| - `core::future::{Future, IntoFuture, ready, pending, poll_fn}`|
| - `core::task::{Context, Poll, Waker, RawWaker}` |
| - `std::task::Wake` & `core::pin::{Pin, pin!}` |
+---------------------------------+---------------------------------+
| (Standardized Interface)
v
+-------------------------------------------------------------------+
| External Async Runtimes (`tokio`, `smol`, `embassy`, `futures`) |
| - Task Executor (`block_on`, `spawn`, `spawn_blocking`) |
| - I/O Reactor (`epoll`, `kqueue`, `IOCP`, `io_uring`, ISRs) |
| - Async I/O & Timers (`TcpStream`, `UdpSocket`, `sleep`) |
| - Async Synchronization (`mpsc`, `oneshot`, `Semaphore`, `select!`) |
+-------------------------------------------------------------------+
Detailed Responsibility Matrix
| Component | In core / std? |
External Runtime / Crate Equivalent |
|---|---|---|
Future & IntoFuture traits |
Yes (std::future) |
Used directly by all runtimes |
AsyncFn, AsyncFnMut, AsyncFnOnce |
Yes (std::ops / prelude) |
Used directly by all runtimes |
Poll, Context, Waker, Wake |
Yes (std::task) |
Used directly by all runtimes |
Pin<P> & pin! macro |
Yes (std::pin) |
Used directly by all runtimes |
AsyncIterator (Stream) |
Nightly std::async_iter |
futures::Stream / tokio_stream::Stream |
Task Executor (block_on, spawn) |
No | tokio::runtime::Runtime, futures::executor::block_on, smol::block_on |
Async TcpStream / TcpListener |
No (std::net is synchronous) |
tokio::net, async_net |
Async Timers (sleep, timeout) |
No (std::thread::sleep blocks) |
tokio::time, async_io::Timer |
Combinators (join!, select!) |
Nightly std::future::join! |
tokio::{join!, select!}, futures::{join!, select!} |
Writing Runtime-Agnostic Async Libraries
When authoring a reusable library crate (such as a protocol parser or middleware layer), avoid hardcoding a dependency on a specific runtime executor whenever possible:
- Depend only on
std::future::Future,std::pin::Pin, andstd::task::{Context, Poll}(or abstract over an I/O trait) so callers usingtokio,smol, orembassycan all use your crate seamlessly:
use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, Waker};
// 100% runtime-agnostic retry helper using only `std::future::Future`:
pub async fn retry_async<F, Fut, T, E>(mut max_attempts: usize, mut operation: F) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
loop {
match operation().await {
Ok(value) => return Ok(value),
Err(err) => {
max_attempts -= 1;
if max_attempts == 0 {
return Err(err);
}
}
}
}
}
fn main() {
let mut attempts = 0;
let fut = retry_async(3, || {
attempts += 1;
let current = attempts;
async move {
if current < 3 {
Err("transient network error")
} else {
Ok("connected on attempt 3!")
}
}
});
let mut pinned = pin!(fut);
let mut cx = Context::from_waker(Waker::noop());
println!("retry_async outcome: {:?}", pinned.as_mut().poll(&mut cx));
}
retry_async outcome: Ready(Ok("connected on attempt 3!"))