Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7. 执行器与运行时 🟡

你将学到:

  • 执行器(executor)做什么:高效地 poll 与休眠
  • 六大主要运行时:mio、io_uring、tokio、async-std、smol、embassy
  • 选择合适运行时的决策树
  • 为何与运行时无关(runtime-agnostic)的库设计很重要

执行器做什么

执行器有两项职责:

  1. 在 future 可以推进时轮询 future
  2. 当没有 future 就绪时高效休眠(使用操作系统 I/O 通知 API)
graph TB
    subgraph Executor["执行器(如 tokio)"]
        QUEUE["任务队列"]
        POLLER["I/O 轮询器<br/>(epoll/kqueue/io_uring)"]
        THREADS["工作线程池"]
    end

    subgraph Tasks
        T1["任务 1<br/>(HTTP 请求)"]
        T2["任务 2<br/>(数据库查询)"]
        T3["任务 3<br/>(文件读取)"]
    end

    subgraph OS["操作系统"]
        NET["网络栈"]
        DISK["磁盘 I/O"]
    end

    T1 --> QUEUE
    T2 --> QUEUE
    T3 --> QUEUE
    QUEUE --> THREADS
    THREADS -->|"poll()"| T1
    THREADS -->|"poll()"| T2
    THREADS -->|"poll()"| T3
    POLLER <-->|"注册/通知"| NET
    POLLER <-->|"注册/通知"| DISK
    POLLER -->|"唤醒任务"| QUEUE

    style Executor fill:#e3f2fd,color:#000
    style OS fill:#f3e5f5,color:#000

mio:基础层

mio(Metal I/O)不是执行器——它是最底层的跨平台 I/O 通知库。它封装了 epoll(Linux)、kqueue(macOS/BSD)和 IOCP(Windows)。

#![allow(unused)]
fn main() {
// Conceptual mio usage (simplified):
use mio::{Events, Interest, Poll, Token};
use mio::net::TcpListener;

let mut poll = Poll::new()?;
let mut events = Events::with_capacity(128);

let mut server = TcpListener::bind("0.0.0.0:8080")?;
poll.registry().register(&mut server, Token(0), Interest::READABLE)?;

// Event loop — blocks until something happens
loop {
    poll.poll(&mut events, None)?; // Sleeps until I/O event
    for event in events.iter() {
        match event.token() {
            Token(0) => { /* server has a new connection */ }
            _ => { /* other I/O ready */ }
        }
    }
}
}

大多数开发者不会直接碰 mio——tokio 和 smol 都构建在它之上。

io_uring:基于完成通知的未来

Linux 的 io_uring(内核 5.1+)代表了与 mio/epoll 所用的就绪式(readiness-based)I/O 模型的根本转变:

Readiness-based (epoll / mio / tokio):
  1. Ask: "Is this socket readable?"     → epoll_wait()
  2. Kernel: "Yes, it's ready"           → EPOLLIN event
  3. App:   read(fd, buf)                → might still block briefly!

Completion-based (io_uring):
  1. Submit: "Read from this socket into this buffer"  → SQE
  2. Kernel: does the read asynchronously
  3. App:   gets completed result with data            → CQE
graph LR
    subgraph "就绪模型(epoll)"
        A1["应用:可读了吗?"] --> K1["内核:是的"]
        K1 --> A2["应用:现在 read()"]
        A2 --> K2["内核:这是数据"]
    end

    subgraph "完成模型(io_uring)"
        B1["应用:帮我读这个"] --> K3["内核:处理中..."]
        K3 --> B2["应用:得到结果 + 数据"]
    end

    style B1 fill:#c8e6c9,color:#000
    style B2 fill:#c8e6c9,color:#000

所有权挑战:io_uring 要求内核在操作完成前拥有缓冲区。这与 Rust 标准 AsyncRead Trait 借用缓冲区的方式冲突。因此 tokio-uring 使用不同的 I/O Trait:

#![allow(unused)]
fn main() {
// Standard tokio (readiness-based) — borrows the buffer:
let n = stream.read(&mut buf).await?;  // buf is borrowed

// tokio-uring (completion-based) — takes ownership of the buffer:
let (result, buf) = stream.read(buf).await;  // buf is moved in, returned back
let n = result?;
}
// Cargo.toml: tokio-uring = "0.5"
// NOTE: Linux-only, requires kernel 5.1+

fn main() {
    tokio_uring::start(async {
        let file = tokio_uring::fs::File::open("data.bin").await.unwrap();
        let buf = vec![0u8; 4096];
        let (result, buf) = file.read_at(buf, 0).await;
        let bytes_read = result.unwrap();
        println!("Read {} bytes: {:?}", bytes_read, &buf[..bytes_read]);
    });
}
方面epoll(tokio)io_uring(tokio-uring)
模型就绪通知完成通知
系统调用epoll_wait + read/write批量 SQE/CQE 环
缓冲区所有权应用保留(&mut buf所有权转移(move buf)
平台Linux、macOS(kqueue)、Windows(IOCP)仅 Linux 5.1+
零拷贝否(用户态拷贝)是(注册缓冲区)
成熟度生产可用实验性

何时使用 io_uring:高吞吐文件 I/O 或网络,且系统调用开销是瓶颈时(数据库、存储引擎、服务 10 万+ 连接的代理)。对大多数应用,带 epoll 的标准 tokio 是正确选择。

tokio:开箱即用的运行时

Rust 生态中占主导地位的异步运行时。Axum、Hyper、Tonic 及大多数生产级 Rust 服务器都在使用它。

// Cargo.toml:
// [dependencies]
// tokio = { version = "1", features = ["full"] }

#[tokio::main]
async fn main() {
    // Spawns a multi-threaded runtime with work-stealing scheduler
    let handle = tokio::spawn(async {
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        "done"
    });

    let result = handle.await.unwrap();
    println!("{result}");
}

tokio 功能:定时器、I/O、TCP/UDP、Unix socket、信号处理、同步原语(Mutex、RwLock、Semaphore、channel)、fs、进程、tracing 集成。

async-std:标准库镜像

用异步版本镜像 std API。不如 tokio 流行,但对初学者更简单。

// Cargo.toml:
// [dependencies]
// async-std = { version = "1", features = ["attributes"] }

#[async_std::main]
async fn main() {
    use async_std::fs;
    let content = fs::read_to_string("hello.txt").await.unwrap();
    println!("{content}");
}

smol:极简运行时

小巧、零依赖的异步运行时。适合希望支持异步又不想拉入 tokio 的库。

// Cargo.toml:
// [dependencies]
// smol = "2"

fn main() {
    smol::block_on(async {
        let result = smol::unblock(|| {
            // Runs blocking code on a thread pool
            std::fs::read_to_string("hello.txt")
        }).await.unwrap();
        println!("{result}");
    });
}

embassy:嵌入式异步(no_std)

面向嵌入式系统的异步运行时。无需堆分配,不要求 std

// Runs on microcontrollers (e.g., STM32, nRF52, RP2040)
#[embassy_executor::main]
async fn main(spawner: embassy_executor::Spawner) {
    // Blink an LED with async/await — no RTOS needed!
    let mut led = Output::new(p.PA5, Level::Low, Speed::Low);
    loop {
        led.set_high();
        Timer::after(Duration::from_millis(500)).await;
        led.set_low();
        Timer::after(Duration::from_millis(500)).await;
    }
}

运行时决策树

graph TD
    START["选择运行时"]

    Q1{"在构建<br/>网络服务器?"}
    Q2{"需要 tokio 生态<br/>(Axum、Tonic、Hyper)?"}
    Q3{"在写库?"}
    Q4{"嵌入式 /<br/>no_std?"}
    Q5{"想要最少<br/>依赖?"}

    TOKIO["🟢 tokio<br/>生态最好,最流行"]
    SMOL["🔵 smol<br/>极简,无生态锁定"]
    EMBASSY["🟠 embassy<br/>嵌入式优先,无 alloc"]
    ASYNC_STD["🟣 async-std<br/>类 std API,适合学习"]
    AGNOSTIC["🔵 与运行时无关<br/>仅用 futures crate"]

    START --> Q1
    Q1 -->|是| Q2
    Q1 -->|否| Q3
    Q2 -->|是| TOKIO
    Q2 -->|否| Q5
    Q3 -->|是| AGNOSTIC
    Q3 -->|否| Q4
    Q4 -->|是| EMBASSY
    Q4 -->|否| Q5
    Q5 -->|是| SMOL
    Q5 -->|否| ASYNC_STD

    style TOKIO fill:#c8e6c9,color:#000
    style SMOL fill:#bbdefb,color:#000
    style EMBASSY fill:#ffe0b2,color:#000
    style ASYNC_STD fill:#e1bee7,color:#000
    style AGNOSTIC fill:#bbdefb,color:#000

运行时对比表

特性tokioasync-stdsmolembassy
生态主导较小极简嵌入式
多线程✅ 工作窃取❌(单核)
no_std
定时器✅ 内置✅ 内置通过 async-io✅ 基于 HAL
I/O✅ 自有抽象✅ std 镜像✅ 通过 async-io✅ HAL 驱动
Channel✅ 丰富通过 async-channel
学习曲线中等高(硬件)
二进制体积极小
🏋️ 练习:运行时对比(点击展开)

挑战:用三种不同运行时(tokio、smol、async-std)写同一个程序。程序应:

  1. 获取 URL(用 sleep 模拟)
  2. 读取文件(用 sleep 模拟)
  3. 打印两个结果

本练习说明 async/await 代码可以相同——只有运行时设置不同。

🔑 解答
// ----- tokio version -----
// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
    let (url_result, file_result) = tokio::join!(
        async {
            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
            "Response from URL"
        },
        async {
            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
            "Contents of file"
        },
    );
    println!("URL: {url_result}, File: {file_result}");
}

// ----- smol version -----
// Cargo.toml: smol = "2", futures-lite = "2"
fn main() {
    smol::block_on(async {
        let (url_result, file_result) = futures_lite::future::zip(
            async {
                smol::Timer::after(std::time::Duration::from_millis(100)).await;
                "Response from URL"
            },
            async {
                smol::Timer::after(std::time::Duration::from_millis(50)).await;
                "Contents of file"
            },
        ).await;
        println!("URL: {url_result}, File: {file_result}");
    });
}

// ----- async-std version -----
// Cargo.toml: async-std = { version = "1", features = ["attributes"] }
#[async_std::main]
async fn main() {
    let (url_result, file_result) = futures::future::join(
        async {
            async_std::task::sleep(std::time::Duration::from_millis(100)).await;
            "Response from URL"
        },
        async {
            async_std::task::sleep(std::time::Duration::from_millis(50)).await;
            "Contents of file"
        },
    ).await;
    println!("URL: {url_result}, File: {file_result}");
}

要点:异步业务逻辑在各运行时上完全相同。只有入口点和定时器/I/O API 不同。这就是为什么编写与运行时无关的库(仅使用 std::future::Future)很有价值。

要点回顾 — 执行器与运行时

  • 执行器的职责:在 waker 唤醒时 poll future,并用 OS I/O API 高效休眠
  • tokio 是服务器默认选择;smol 适合最小体积;embassy 面向嵌入式
  • 业务逻辑应依赖 std::future::Future,而非特定运行时
  • io_uring(Linux 5.1+)是高性能 I/O 的未来,但生态仍在成熟中

另见: 第 8 章 — Tokio 深入 了解 tokio 细节,第 9 章 — 何时不该用 Tokio 了解替代方案