Rust 的异步生态有个独特之处:语言只提供 async/await 语法和 Future trait,运行时(runtime)由第三方库实现。tokio、async-std、smol 是三个主要选择,设计哲学差异很大。这篇文章从 API、性能、生态三个维度做对比,帮你决定该用哪个。
设计哲学
tokio:功能全面的工业级运行时。提供任务调度、I/O 驱动、定时器、同步原语、Channel 等全套设施。目标是做异步编程的"标准库"。
async-std:镜像标准库 API 的异步运行时。async_std::fs 对标 std::fs,async_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 的极简优势。除非已有项目在用,否则新项目不太推荐。