Rust异步运行时对比:tokio vs async-std vs smol

Rust 的异步生态有个独特之处:语言只提供 async/await 语法和 Future trait,运行时(runtime)由第三方库实现。tokio、async-std、smol 是三个主要选择,设计哲学差异很大。这篇文章从 API、性能、生态三个维度做对比,帮你决定该用哪个。

设计哲学

tokio:功能全面的工业级运行时。提供任务调度、I/O 驱动、定时器、同步原语、Channel 等全套设施。目标是做异步编程的"标准库"。

async-std:镜像标准库 API 的异步运行时。async_std::fs 对标 std::fsasync_std::net 对标 std::net。目标是让标准库用户零学习成本迁移到异步。

smol:极简主义。核心只有一个 executor 和一个 reactor,所有高级功能通过组合小 crate 实现。总代码量不到 tokio 的 1/10。

用一个比喻:tokio 是 Spring Boot(全家桶),async-std 是 Java EE(标准化),smol 是 Express.js(最小核心 + 中间件)。

API 对比

启动运行时

// tokio
#[tokio::main]
async fn main() {
    println!("tokio");
}

// async-std
#[async_std::main]
async fn main() {
    println!("async-std");
}

// smol
fn main() {
    smol::block_on(async {
        println!("smol");
    });
}

TCP Server

// === tokio ===
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    loop {
        let (mut socket, _) = listener.accept().await?;
        tokio::spawn(async move {
            let mut buf = [0u8; 1024];
            let n = socket.read(&mut buf).await.unwrap();
            socket.write_all(&buf[..n]).await.unwrap();
        });
    }
}

// === async-std ===
use async_std::net::TcpListener;
use async_std::prelude::*;
use async_std::task;

#[async_std::main]
async fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    let mut incoming = listener.incoming();
    while let Some(stream) = incoming.next().await {
        let mut stream = stream?;
        task::spawn(async move {
            let mut buf = [0u8; 1024];
            let n = stream.read(&mut buf).await.unwrap();
            stream.write_all(&buf[..n]).await.unwrap();
        });
    }
    Ok(())
}

// === smol ===
use smol::net::TcpListener;
use smol::io::{AsyncReadExt, AsyncWriteExt};

fn main() -> std::io::Result<()> {
    smol::block_on(async {
        let listener = TcpListener::bind("127.0.0.1:8080").await?;
        loop {
            let (mut stream, _) = listener.accept().await?;
            smol::spawn(async move {
                let mut buf = [0u8; 1024];
                let n = stream.read(&mut buf).await.unwrap();
                stream.write_all(&buf[..n]).await.unwrap();
            }).detach();
        }
    })
}

API 风格上 tokio 和 smol 比较接近,async-std 最贴近标准库。

定时器

// tokio
tokio::time::sleep(Duration::from_secs(1)).await;

// async-std
async_std::task::sleep(Duration::from_secs(1)).await;

// smol (用 async-io crate)
smol::Timer::after(Duration::from_secs(1)).await;

Channel

// tokio 自带 mpsc channel
let (tx, mut rx) = tokio::sync::mpsc::channel(100);

// async-std 用 futures 的 channel
use futures::channel::mpsc;
let (tx, rx) = mpsc::channel(100);

// smol 推荐用 async-channel crate
let (tx, rx) = async_channel::bounded(100);

tokio 的同步原语(Mutex、RwLock、Semaphore、Notify)是自带的,性能针对异步场景优化过。async-std 和 smol 需要依赖外部 crate(如 async-lock)。

性能基准

以下是几个典型场景的粗略对比(具体数字取决于硬件和配置,仅供参考):

场景 tokio async-std smol
任务创建/调度 (ns) ~50 ~80 ~30
TCP echo 吞吐 (req/s) 中高
10K 并发连接内存 (MB) ~40 ~60 ~30
编译时间(clean build) 较长

关键发现:

  • smol 因为极简,任务创建开销最小,内存占用最低
  • tokio 在高并发网络 I/O 场景下吞吐量最高,因为它的 epoll/kqueue 驱动深度优化过
  • async-std 各方面中规中矩
  • 编译时间 smol < async-std < tokio(tokio 依赖多)

对于大多数应用来说,性能差异不是瓶颈。你的业务逻辑、数据库查询、网络延迟远比运行时本身的开销大。

生态兼容性

这才是选择运行时最重要的因素。

tokio 生态

  • hyper / axum / tonic (HTTP/gRPC)
  • sqlx / sea-orm (数据库)
  • reqwest (HTTP 客户端)
  • tower (中间件抽象)
  • tracing (结构化日志)

依赖 tokio 的知名项目:Cloudflare Workers、Discord、AWS Lambda Runtime、Linkerd。

async-std 生态

  • tide (HTTP 框架)
  • surf (HTTP 客户端)

smol 生态

  • 主要通过 async-compat 层兼容 tokio 生态
  • 自有生态较小

现实是:tokio 的生态碾压式领先。绝大多数生产级 Rust 异步库都依赖 tokio 运行时。如果你选了 async-std 或 smol,很多库用不了或者要加兼容层。

如何选择

场景 推荐
Web 服务、API 后端 tokio(生态最好)
需要与 tokio 库集成 tokio(不然要兼容层)
嵌入式或资源受限环境 smol(最小最轻)
教学目的、理解异步原理 smol(代码简单易读)
想要最接近标准库的 API async-std
CLI 工具中的简单异步需求 smol 或 tokio(都行)

我的建议很直接:大多数情况选 tokio。不是因为它技术上最优,而是因为生态锁定效应太强了。你选了其他运行时,迟早会遇到某个库只支持 tokio 的情况。

如果你在做特殊场景(嵌入式、极致资源优化),或者只是想学习异步运行时的原理,smol 是一个优秀的选择。它的源码量小到你可以通读,理解整个运行时的工作机制。

async-std 目前处于比较尴尬的位置:维护活跃度下降,生态规模远不如 tokio,也没有 smol 的极简优势。除非已有项目在用,否则新项目不太推荐。