On this page

tokio Runtime

tokio’s reactor (based on epoll/kqueue/IOCP) handles async I/O, and the executor schedules Futures across multiple threads using work-stealing—the combination forms the de facto standard runtime for Rust async. spawn submits Futures to a thread pool, select! waits on multiple async operations simultaneously—understanding the division of labor between reactor and executor reveals tokio’s performance model.

tokio’s Three-Layer Architecture

flowchart TD
    TASK["🧵 Your async task<br/>tokio::spawn"]

    TASK --> QUEUE["📋 Task Queue<br/>Futures waiting to be scheduled"]

    QUEUE --> EXECUTOR["⚙️ Executor<br/>work-stealing scheduler<br/>one worker thread per CPU core"]

    EXECUTOR --> REACTOR["🔌 Reactor<br/>epoll (Linux) / kqueue (macOS)<br/>IOCP (Windows)"]

    REACTOR --> KERNEL["🐧 OS Kernel"]

    REACTOR -.->|"I/O ready → wake task"| QUEUE

    classDef user fill:#e3f2fd,stroke:#1565c0
    classDef middle fill:#fff3e0,stroke:#ef6c00
    classDef os fill:#f3e5f5,stroke:#7b1fa2
    class TASK,QUEUE user
    class EXECUTOR,REACTOR middle
    class KERNEL os

Reactor: Listens to all registered I/O sources (sockets, timers, signals) → when I/O is ready → notifies the corresponding task to wake up. Executor: Maintains the task queue → woken tasks regain CPU time. Your code: Simply calls async fn and uses .await, without directly interacting with the reactor or executor.

Two Types of Runtimes

// Multi-threaded (default)
#[tokio::main]
async fn main() { ... }

// Equivalent to:
fn main() {
    tokio::runtime::Builder::new_multi_thread()
        .worker_threads(num_cpus::get())  // Default: number of CPU cores
        .enable_all()                     // Enable I/O + time driver
        .build().unwrap()
        .block_on(async { ... });
}

// Single-threaded (for simple scenarios, e.g., no need for cross-thread synchronization)
#[tokio::main(flavor = "current_thread")]
async fn main() { ... }

spawn: Independent Concurrent Tasks

let handle = tokio::spawn(async move {
    let result = do_heavy_work().await;
    println!("{}", result);
});
// handle: JoinHandle — you can .await it to get the return value
let result = handle.await.unwrap();

Tasks spawned via spawn can execute on different worker threads—they are truly concurrent, not just coroutines. The returned JoinHandle is itself a Future; .awaiting it waits for the task to complete.

Work-Stealing Scheduler

Each worker thread has its own local task queue. When a worker becomes idle, it randomly steals tasks from other workers. This model is more efficient than “one global queue + all workers competing”—it reduces lock contention because local push/pop operations are lock-free.

select!: Waiting on Multiple Futures

tokio::select! {
    result = long_running_op() => { /* handle result */ }
    _ = tokio::signal::ctrl_c() => { /* Ctrl+C signal */ }
}

Once any branch completes → the other branches are cancelled (dropped). This is async Rust’s built-in cancellation model—no explicit cancellation token is needed. The Drop implementation of the cancelled Future is responsible for cleaning up resources.

References

  • tokio: docs.rs/tokio, tokio.rs
  • async book: rust-lang.github.io/async-book

Keywords: tokio, reactor, executor, work stealing, spawn, select!, runtime, current_thread