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/IOCP network reactors, and a blocking thread pool.
  • Bare-metal microcontrollers (embassy): Have 0 OS threads, 0 heap allocator (#![no_std]), and wake tasks directly from hardware interrupt service routines (ISRs).
  • Linux io_uring storage engines (monoio, glommio): Use a thread-per-core shared-nothing architecture where futures are intentionally !Send.
  • WebAssembly in the browser (wasm-bindgen-futures): Bridges Rust Futures directly onto the browser's single-threaded JavaScript Promise microtask 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, and std::task::{Context, Poll} (or abstract over an I/O trait) so callers using tokio, smol, or embassy can 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!"))