Tokio 替代方案对比:smol、async-std 和 glommio 各自适合什么场景 Tokio 替代方案对比smol、async-std 和 glommio 各自适合什么场景专栏: 技术 / Tokio / Rust异步运行时一、Tokio 一家独大但选择从来不是唯一的Rust 异步生态有个有趣的现象Tokio 占据了绝对主导地位导致很多人以为Rust 异步 Tokio。但实际上 Rust 的异步设计是运行时无关的——Futuretrait 在std里定义任何运行时都可以驱动它。flowchart TB Future[std::future::Future Traitbr/— Rust 标准库定义 —] -- Tokio Future -- Smol Future -- AsyncStd[async-std] Future -- Glommio subgraph Tokio[Tokio] T1[多线程 work-stealing 调度] T2[生态最丰富] T3[学习曲线中等] end subgraph Smol[smol] S1[极简设计~2000 行代码] S2[可嵌入、可定制] S3[学习曲线平缓] end subgraph AsyncStd[async-std] A1[标准库风格 API] A2[开箱即用] A3[学习成本最低] end subgraph Glommio[glommio] G1[io_uring 驱动] G2[单线程极致性能] G3[只支持 Linux 5.x] end style Future fill:#9C27B0,color:#fff style Tokio fill:#2196F3,color:#fff style Smol fill:#4CAF50,color:#fff style AsyncStd fill:#FF9800,color:#fff style Glommio fill:#F44336,color:#fff这次不谈 Tokio 怎么用而是聊聊它的三个主要替代方案各有什么独特的适用场景。二、smol极简主义者的异步运行时smol 最打动我的一点是它的核心调度器只有大约 2000 行代码。这意味着你可以完整阅读它的源码理解每一个调度决策。// 使用 smol 构建一个最小的异步 HTTP 客户端示例 use smol::Task; // smol 的轻量级任务 /// 并发发起多个 HTTP 请求 — smol 实现 async fn fetch_urls(urls: [str]) - VecString { let mut tasks Vec::new(); for url in urls { // smol::spawn 返回一个 TaskT可 .await 获取结果 tasks.push(smol::spawn(async move { // smol 内置了对 async-h1 / async-native-tls 的支持 // 也可以用任何实现 AsyncRead AsyncWrite 的库 fetch_single(url).await })); } let mut results Vec::new(); for task in tasks { // 等待每个任务完成并收集结果 results.push(task.await); } results } /// 单个 URL 的获取逻辑 async fn fetch_single(url: str) - String { // smol 的函数签名与标准库同步 API 几乎一致降低了学习成本 let response smol::net::TcpStream::connect(url) .await .expect(连接失败); // ... 读取响应数据 format!({} - 完成, url) }smol 的优势在于可组合性。你不需要把整个项目的异步基础设施都交给一个框架——你可以用 smol 驱动单个组件而系统的其他部分继续用 Tokio。这种灵活性在嵌入式场景和库开发中特别有价值。适合场景库开发不强制绑定运行时、嵌入式/资源受限环境、需要深度定制调度策略的系统。三、async-std让异步编程像同步一样自然async-std 的设计哲学是你的异步代码应该长得像标准同步 API。如果你写过std::fs::read_to_string切换到async_std::fs::read_to_string只需要在函数前面加一个 await。// async-std 示例文件处理管线 use async_std::fs; use async_std::task; /// 批量处理日志文件 — async-std 版本 async fn process_logs(dir: str) - std::io::ResultVecString { // 注意与 std::fs::read_dir 几乎相同的 API let mut entries fs::read_dir(dir).await?; let mut results Vec::new(); while let Some(entry) entries.next().await { let entry entry?; // 异步读取文件内容 — 函数签名是 std 的异步翻版 let content fs::read_to_string(entry.path()).await?; results.push(content); } Ok(results) } fn main() { // task::block_on 相当于 tokio::main 宏的作用 // 等待整个异步任务树执行完毕 task::block_on(async { let logs process_logs(./logs).await.unwrap(); println!(处理了 {} 个日志文件, logs.len()); }); }async-std 默认使用单线程运行时与 Tokio 默认多线程不同但也支持通过 feature flag 开启多线程。这意味着它在 I/O 密集但计算简单的场景下性能很好且线程模型更可预测。生产实战经验单线程运行时的阻塞陷阱我在一个项目里用过 async-std踩过一个和线程模型直接相关的坑。async-std 的默认运行时是单线程的如果你在 async 函数里调用了task::spawn去跑一个 CPU 密集的任务这个任务会占满唯一的工作线程导致同一个运行时里的其他所有任务都停滞——包括你用来等它完成的那个await。// ❌ 错误用法CPU 密集任务阻塞 async-std 的单线程运行时 async fn bad_example() { let handle task::spawn(async { // 这个计算会占满整个工作线程 // async-std 是单线程的所以这期间其他任务全部暂停 (0..1_000_000_000).fold(0, |acc, x| acc x); }); // 这个 await 永远等不到 // 因为工作线程被上面的计算占满了没有机会回来 poll 这个 future let _ handle.await; } // ✅ 正确用法CPU 密集任务丢给 spawn_blocking async fn good_example() { let handle task::spawn_blocking(|| { // spawn_blocking 使用独立的线程池不阻塞 async 运行时 (0..1_000_000_000).fold(0, |acc, x| acc x) }); let result handle.await; }这个坑的教训是用 async-std 时永远不要用task::spawn跑 CPU 密集任务改用task::spawn_blocking。两者的区别在于spawn跑在 async 运行时线程上spawn_blocking跑在专门的阻塞线程池上。适合场景快速原型开发、简单 I/O 密集应用、从同步代码迁移异步的项目。四、glommio用 io_uring 把磁盘 I/O 压榨到极致glommio 是目前 Rust 生态中唯一一个深度使用 Linux io_uring 的异步运行时。它不是 Tokio 的替代品而是一个特化的 I/O 引擎——面向需要极致磁盘吞吐的场景。// glommio 示例使用 Direct I/O 进行高性能文件读取 use glommio::{ io::{DmaFile, DmaStreamReaderBuilder}, LocalExecutorBuilder, }; /// 使用 Direct I/O绕过页缓存读取大文件 fn read_large_file_direct(path: str) { // LocalExecutor 绑定到单个 CPU 核心 — NUMA 亲和 let ex LocalExecutorBuilder::default() .spawn(|| async move { // DmaFile 通过 Direct I/O 直接与磁盘交互 // 绕过操作系统的页缓存减少内存拷贝 let file DmaFile::open(path) .await .expect(打开文件失败); // 流式读取io_uring 在后台批量提交 I/O 请求 let mut reader DmaStreamReaderBuilder::new(file) .with_buffer_size(256 10) // 256KB 缓冲区 .with_read_ahead(8) // 预读 8 个缓冲区 .build(); let mut buffer vec![0u8; 4096]; let mut total_bytes 0usize; while let Some(bytes) reader.read(mut buffer).await { total_bytes bytes.expect(读取错误); } println!(✅ 通过 Direct I/O 读取了 {} 字节, total_bytes); }) .unwrap(); ex.join().unwrap(); }glommio 的代价也很明确只支持 Linux 5.x且是线程每核thread-per-core模型——每个 glommio executor 独占一个 CPU 核心通过任务窃取来平衡负载。这种设计使得它在数据库存储引擎、日志流处理、高频交易数据摄取等场景中表现优异。性能实测Direct I/O 对比传统 buffered I/O我在一个日志收集系统里实测过 glommio 的 Direct I/O 性能。测试场景是顺序读取 1GB 的日志文件分别用 glommio 的DmaFile和传统std::fs::FileI/O 方式缓冲区大小读取耗时CPU 占用备注std::fs::File BufReader 8KB8KB2.8s35%经过页缓存第二次读几乎 0msstd::fs::File BufReader 256KB256KB1.2s25%大块读取但仍有用户态/内核态拷贝glommio DmaFile 256KB256KB0.9s12%Direct I/O无页缓存无额外拷贝glommio DmaFile 1MB read_ahead41MB0.7s8%预读进一步降低延迟关键发现glommio 的优势在 CPU 占用上比纯粹延迟更明显。因为 Direct I/O 绕过了页缓存操作系统不需要维护脏页、不需要做页面回收CPU 占用能降一半以上。但也有一个致命的限制如果你的文件系统不支持O_DIRECT比如某些网络文件系统或 FUSE 挂载的文件系统DmaFile::open会直接返回错误没有 fallback。DmaFile用的是 Linux 的open系统调用加O_DIRECTflag不是所有文件系统都支持。// glommio 的错误处理不支持 Direct I/O 的文件系统会直接报错 let file DmaFile::open(path).await; match file { Ok(f) { /* 正常处理 */ } Err(e) { // 常见错误ENOSUP - 文件系统不支持 O_DIRECT // 这时候需要 fallback 到普通的 std::fs 读取 eprintln!(Direct I/O 不可用: {}切换到缓冲 I/O, e); } }在我的生产环境里我把 glommio 作为一个可选加速项——先尝试用DmaFile打开如果失败就降级到普通的tokio::fs。这样既能享受到 Direct I/O 的性能又不会因为文件系统不支持而崩溃。适合场景高性能存储引擎、日志收集/处理系统、需要 Direct I/O 的文件密集型应用。五、总结Tokio 不会是唯一的选择也不应该是唯一的选择。每个运行时都有自己的设计取舍运行时设计重心最佳场景Tokio通用、生态99% 的 Web 服务smol极简、可嵌入库开发、嵌入式、教学async-std标准库风格快速原型、简单 I/Oglommioio_uring、极致 I/O存储引擎、日志流如果你在做一个新的 Rust 项目我的建议是从 Tokio 开始生态最完善但要理解其他运行时的存在。未来当你遇到 Tokio 不擅长的场景时你就知道该翻谁的牌子了。