- 后端
【免费下载链接】smol
A small and fast async runtime for Rust
导读
本文以 CHANGELOG.md 为骨架,系统梳理 smol 从 0.1.0 一路演进到 2.0.2 的完整技术脉络:包括"单体内核 → 子 crate 生态"的架构大分裂、事件通知机制的跨平台修复、SMOL_THREADS环境变量的引入与回归、block_on()与smol::run()的交替变迁,以及 2.0.0 的破坏性升级。通过结合当前仓库的 src/lib.rs、src/spawn.rs、Cargo.toml 与 examples 目录中的真实代码,读者既能看清每个版本号背后的设计决策,也能掌握当前版本(2.0.2)下smol::spawn、block_on、Executor、各模块重导出的实际用法与配置要点。
一、前置地图:smol 是什么,CHANGELOG 在记录什么
smol 的项目自我定位是 "A small and fast async runtime"(一个小巧而快速的异步运行时)。在 README.md 与 src/lib.rs 的开头注释中都有一句关键的话:"This crate simply re-exports other smaller async crates"——这个 crate 本质上是若干更小异步 crate 的组合与再导出层。
这决定了理解 CHANGELOG 的正确姿势:smol 的变更史,绝大部分是"组合层"的变更史,而它背后的底层能力分散在 async-io、async-executor、async-net、async-fs、blocking、futures-lite 等子 crate 中。
当前仓库的版本与关键基线(Cargo.toml):
| 项目 | 当前值 |
|---|---|
| 版本 | 2.0.2 |
| edition | 2021 |
| rust-version(MSRV) | 1.85 |
| 许可 | Apache-2.0 OR MIT |
| 运行时依赖 | async-channel 2、async-executor 1.5、async-fs 2、async-io 2.1、async-lock 3、async-net 2、blocking 1.3、futures-lite 2(另按平台引入 async-process 2) |
当前 src/lib.rs 的再导出结构,正是 CHANGELOG 中 v0.2.0 拆分、v0.4.0 补模块之后沉淀下来的形态:
Executor、LocalExecutor、Task←async_executorblock_on、Async、Timer←async_iounblock、Unblock←blockingfuture、io、pin、ready、stream←futures_litechannel←async_channel,fs←async_fs,lock←async_lock,net←async_netprocess←async_process(在非 espidf 平台下启用,见 src/lib.rs)
下文按 CHANGELOG 的时间线展开,每一节都会把对应版本的完整条目列出并展开解读。
二、0.1.x 时代:单体内核的打磨(0.1.0 ~ 0.1.18)
2.1 起步阶段:文档与连接语义的初修(0.1.0 ~ 0.1.4)
# Version 0.1.0 - Initial release # Version 0.1.1 - Upgrade dependencies # Version 0.1.2 - Improved internal docs, fixed typos, and more comments # Version 0.1.3 - Fix the writability check in async connect - More comments and documentation - Better security advice on certificates # Version 0.1.4 - Fix a bug in UDS async connect0.1.0 是首次发布。此后 0.1.1 只是例行依赖升级;0.1.2 与 0.1.3 把重心放在文档与注释质量上,同时 0.1.3 修复了async connect 的"可写性(writability)检查"——异步连接能否成功的判断依赖正确的写就绪语义,这是所有 TCP/UDP 异步连接的基础正确性问题;0.1.4 又修补了Unix Domain Socket(UDS)异步连接中的另一个 bug。这一阶段的主题可以概括为"连接语义的正确性 + 文档基建",为后续的底层机制重构打基础。
2.2 阻塞 I/O、eventfd 与一次性通知的引入(0.1.5 ~ 0.1.8)
# Version 0.1.5 - Use `blocking` crate for blocking I/O. - Fix a re-registration bug when in oneshot mode. - Use eventfd on Linux. - More tests. - Fix timeout rounding error in epoll/wepoll. # Version 0.1.6 - Fix a deadlock by always re-registering `IoEvent`. # Version 0.1.7 - Update `blocking` to `0.4.2`. - Make `Task::blocking()` work without `run()`. # Version 0.1.8 - Revert the use of `blocking` crate.这一阶段有三条值得展开的技术线索:
- 引入
blockingcrate 处理阻塞 I/O:文件读取、DNS 解析、std阻塞调用不能直接放在 reactor 线程上,必须交给线程池。0.1.5 首次接入,0.1.8 又"回退"(Revert)了这次使用——说明当时对"何时引入阻塞线程池"做过反复取舍。最终在 v0.2.0 拆分时,blocking成为独立的子 crate 固定下来,如今它通过unblock/Unblock接口被 src/lib.rs 再导出,当前示例 examples/get-request.rs 中的smol::unblock(...)与Unblock::new(std::io::stdout())就是它的实际用法。 - Linux 使用 eventfd 作为唤醒原语:eventfd 是 Linux 上轻量的事件通知 fd,用它做 reactor 唤醒比自建的管道/条件变量更廉价。0.1.5 引入,0.1.9 中"eventfd 未关闭"的泄漏修复也与之相关。
- oneshot 模式下的重注册 bug(0.1.5)与
IoEvent死锁(0.1.6):当通知采用一次性(oneshot)模式时,每次触发后必须重新注册才能收到下一次事件,忘记重注册会丢失后续唤醒,甚至导致死锁。0.1.6 明确写"总是重新注册IoEvent"来修复死锁。
另外 0.1.5 还修复了epoll/wepoll 的超时取整(rounding)误差——轮询超时从毫秒换算到内核时间单位时若舍入不当,会导致实际睡眠时间与期望不符;wepoll 正是 Windows 平台上模拟 epoll 语义的关键组件。
0.1.7 让Task::blocking()在不存在run()时也能工作,使阻塞任务不再强依赖一个显式的运行时入口。
2.3 通知机制革命:全平台 oneshot 与 Windows 100% CPU 修复(0.1.9 ~ 0.1.11)
# Version 0.1.9 - Switch to oneshot-style notifications on all platforms. - Fix a bug that caused 100% CPU usage on Windows. - Deprecate `Async::with()` and `Async::with_mut()`. - Add `Async::read_with()`, `Async::read_with_mut()`, `Async::write_with()`, and `Async::write_with_mut()`. - Fix a bug where eventfd was not closed. # Version 0.1.10 - Fix incorrectly reported error kind when connecting fails. # Version 0.1.11 - Update `wepoll-binding`. - Reduce dependencies. - Replace `nix` with `libc`. - Set minimum required `tokio` version to 0.2.0.1.9 是整个 0.1.x 阶段最重要的版本之一,核心是三件事:
- 所有平台统一切换为 oneshot 式通知。在此之前各平台的通知语义不完全一致,统一之后每次事件触发只交付一次、需要显式重注册,语义清晰、唤醒精确,同时修复了Windows 上 100% CPU 占用的 bug(可以推断:之前 Windows 上通知触发后未正确消费/重置,导致 reactor 循环空转忙等)。
- API 换代:弃用
Async::with()/Async::with_mut(),新增Async::read_with()、Async::read_with_mut()、Async::write_with()、Async::write_with_mut()。新的with系列把"读/写能力判断"与"底层 fd 操作"组合在一个 API 中,调用者可以安全地拿到原始句柄执行非阻塞操作。这一 API 至今仍是核心用法,当前仓库的示例大量使用它:- examples/linux-inotify.rs 用
source.read_with(|_| read_op(&mut inotify))等待 inotify 事件; - examples/linux-timerfd.rs 用
Async::new(timer)?.read_with(...)睡眠直至 OS 定时器触发; - examples/windows-uds.rs 用
listener.read_with(|l| l.accept())在 Windows 上模拟 Unix socket 的异步 accept。
- examples/linux-inotify.rs 用
- 修复 eventfd 泄漏:资源(fd)泄漏在这种长生命周期运行时里是致命的,逐条关闭 eventfd 是长期稳定性的一部分。
0.1.10 修复了连接失败时错误 kind 报告不准确的问题;0.1.11 则开始了依赖瘦身(Reduce dependencies),用更底层、更精简的libc替换nix,更新 wepoll-binding,并把最低tokio版本约束为 0.2——这些都是为了缩小编译面与维护面。
2.4 丢失唤醒与公平性:executor 核心稳定性攻坚(0.1.12 ~ 0.1.15)
# Version 0.1.12 - Fix a bug in `Async::<UdpSocket>::recv()`. # Version 0.1.13 - Fix deadlocks caused by lost wakeups. - Refactor the executor. # Version 0.1.14 - Clear the flag after every call to `react()`. # Version 0.1.15 - Fix wakeups lost inside the executor. - Fix a fairness issue in the executor.这一阶段聚焦 executor 的正确性,关键词是"丢失唤醒"(lost wakeups):
- 0.1.12 修了
Async::<UdpSocket>::recv()的一个 bug(UDP 无连接语义下收发状态管理的细节错误); - 0.1.13 修复由丢失唤醒引发的死锁,并重构 executor;
- 0.1.14 要求"每次调用
react()后清除标志位"——事件标志的 set/clear 时序若不对,就会出现要么丢唤醒、要么空转的两种病态; - 0.1.15 继续修复 executor 内部的丢失唤醒,并解决公平性(fairness)问题——即任务队列调度不能让某些任务被长时间饿死。
可以推断,这一连串修复最终沉淀为async-executor子 crate 的健壮内核,也正是 v0.2.0 敢于把 executor 拆出去独立演进的前提。
2.5 平台兼容性扩展与 Rust 1.39 支持(0.1.16 ~ 0.1.18)
# Version 0.1.16 - Add `Async::readable()` and `Async::writable()`. # Version 0.1.17 - Support more platforms by changing `AtomicU64` to `AtomicUsize`. - Remove `IoEvent` and simplify reactor notification. # Version 0.1.18 - Support Rust 1.39.00.1.16 新增Async::readable()与Async::writable()两个判断句柄读写就绪的底层方法,它们是read_with/write_with实现的基础。0.1.17 把内部计数原子类型从AtomicU64换成AtomicUsize——usize在更多 32 位/嵌入式目标上可用,扩大了平台覆盖;同时移除IoEvent并简化 reactor 通知,说明 0.1.9 之后的 oneshot 通知架构已经足够统一,不再需要单独的IoEvent类型。0.1.18 把 MSRV 拉到 Rust 1.39.0,让更多旧工具链可以直接使用。
2.6 0.1.x 阶段小结
| 版本 | 主题 | 关键动作 |
|---|---|---|
| 0.1.0 | 首发 | Initial release |
| 0.1.1 | 依赖 | Upgrade dependencies |
| 0.1.2 | 文档 | Improved internal docs, fixed typos, more comments |
| 0.1.3 | 连接语义 | Fix writability check in async connect;安全建议 |
| 0.1.4 | 连接语义 | Fix UDS async connect |
| 0.1.5 | 底层机制 | 引入 blocking crate、eventfd、修 oneshot 重注册与 epoll/wepoll 超时取整 |
| 0.1.6 | 底层机制 | 总是重注册IoEvent修死锁 |
| 0.1.7 | 阻塞任务 | Task::blocking()不再依赖run() |
| 0.1.8 | 依赖策略 | 回退 blocking crate 的使用 |
| 0.1.9 | 通知机制 | 全平台 oneshot、修 Windows 100% CPU、弃用with系列、新增read_with系列、修 eventfd 泄漏 |
| 0.1.10 | 错误处理 | 修连接失败错误 kind 报告 |
| 0.1.11 | 依赖瘦身 | 更新 wepoll-binding、nix→libc、约束 tokio 最低 0.2 |
| 0.1.12 | 网络修复 | FixAsync::<UdpSocket>::recv() |
| 0.1.13 | executor | 修丢失唤醒死锁、重构 executor |
| 0.1.14 | executor | 每次react()后清标志 |
| 0.1.15 | executor | 修 executor 内丢失唤醒、修公平性 |
| 0.1.16 | API | 新增Async::readable()/writable() |
| 0.1.17 | 平台 | AtomicU64→AtomicUsize、移除IoEvent |
| 0.1.18 | 兼容 | Support Rust 1.39.0 |
三、0.2.0:架构大分裂——拆出子 crate 生态
# Version 0.2.0 - Split `smol` into `async-io`, `blocking`, and `multitask`. - Big breaking change - there is now only one type `Task`.0.2.0 是 smol 历史上最重要的转折点:把单体内核拆成async-io、blocking、multitask三个子 crate。这一决策直接定义了 smol 今天的形态——正如 README.md 列出的子 crate 生态所示,polling负责跨平台轮询(epoll/kqueue/event ports/wepoll),async-io提供Async适配器与定时器,async-executor提供可组合的执行器,async-task提供任务抽象,blocking提供阻塞线程池,futures-lite是futures的轻量分支,async-net/async-fs/async-process/async-lock/async-channel分别覆盖网络、文件、进程、锁与通道。
同时 0.2.0 是一次大破坏性变更(Big breaking change):任务类型从此只有一种Task("there is now only one typeTask"),不再有运行时风格各异的多种任务类型。这一简化被保留至今——当前 src/lib.rs 仍只再导出async_executor::{Executor, LocalExecutor, Task}一组任务类型。
四、0.3.x:重新聚合,引入 block_on 与 SMOL_THREADS
# Version 0.3.0 - Reexport `futures-lite`, `blocking`, `async-executor`. - Re-introduce `smol::run()`. # Version 0.3.1 - Fix some typos in docs. # Version 0.3.2 - Reexport `FutureExt`. # Version 0.3.3 - Add `block_on()`. - Use `SMOL_THREADS` environment variable.拆分之后,0.3.x 开始"重新聚合":0.3.0 把futures-lite、blocking、async-executor作为公开再导出重新放回用户面前,并重新引入smol::run();0.3.2 再导出FutureExt便于直接调用Future扩展方法;0.3.1 修文档错别字。
0.3.3 是塑造当前 API 的关键版本:
- 新增
block_on():直接阻塞当前线程驱动一个 future。当前 src/lib.rs 中的block_on即由async_io::block_on再导出,examples/get-request.rs、examples/tcp-client.rs、examples/chat-server.rs 都以smol::block_on(async { ... })作为程序入口。与run()相比,block_on只在当前线程执行,不涉及多线程调度,是编写小工具与测试时最轻量的入口。 - 引入
SMOL_THREADS环境变量:控制全局执行器的后台线程数量。这一环境变量在 0.4.x 经历过"移除—恢复"的波折(见下一节),但它至今仍活在 src/spawn.rs 中:
let num_threads = { // Parse SMOL_THREADS or default to 1. std::env::var("SMOL_THREADS") .ok() .and_then(|s| s.parse::<usize>().ok()) .filter(|&n| n > 0) .unwrap_or(1) };从源码看,SMOL_THREADS的解析规则是:取环境变量 → 成功解析为usize→ 且值大于 0,否则默认1(单线程)。线程数n会生成名为smol-1…smol-n的后台线程(src/spawn.rs)。在真实部署中,你可以在启动进程前设置:
SMOL_THREADS=4 cargo run --release --bin your-app来让全局执行器以 4 个线程运行任务;不设置则默认单线程。
五、0.4.x:模块化补齐与 smol::run() 的告别
# Version 0.4.0 - Add `process`, `fs`, `net`, `lock`, `channel` modules. - Update all dependencies - Remove `smol::run()`. # Version 0.4.1 - Bring back `SMOL_THREADS`. # Version 0.4.2 - Update dependencies.0.4.0 把 smol 从一个"执行器 + I/O 适配器"补全为全功能运行时:
- 新增
process、fs、net、lock、channel五个模块,分别对应子进程管理、文件系统、TCP/UDP/Unix 网络、异步锁(mutex/barrier/rwlock/semaphore)、多生产者多消费者通道。这些模块的再导出关系直接体现在当前 src/lib.rs 中:async_channel as channel、async_fs as fs、async_lock as lock、async_net as net、async_process as process。- 网络模块的实战形态见 examples/chat-server.rs(
Async::<TcpListener>::bind(...))与 examples/hyper-client.rs(net::TcpStream::connect(...)); - 通道模块的实战形态见 examples/web-crawler.rs(
async_channel::bounded(200)做并发限流与结果回收)与 examples/ctrl-c.rs(用有界通道把 Ctrl-C 信号转成异步消息)。
- 网络模块的实战形态见 examples/chat-server.rs(
- 移除
smol::run():block_on彻底取代了run()的入口地位。这一"先引入、再移除"的过程说明项目最终选择了"显式阻塞当前线程 + 按需使用多线程执行器"的模型,而非隐式的多线程全局运行时。 - 0.4.1恢复
SMOL_THREADS(说明 0.4.0 重构时一度把它弄丢了又找回来),0.4.2 例行更新依赖。
六、1.x:稳定之后的维护期
# Version 1.0.0 - Stabilize. # Version 1.0.1 - Update dependencies. # Version 1.1.0 - Update `async-executor`. # Version 1.2.0 - Update all dependencies. # Version 1.2.1 - Temporarily downgrade `async-executor`. # Version 1.2.2 - Bump version for docs.rs to pick up latest dependencies. # Version 1.2.3 - Bump version for docs.rs to pick up latest dependencies. # Version 1.2.4 - Update dependencies. # Version 1.2.5 - Bump version for docs.rs to pick up latest dependencies. # Version 1.3.0 - Remove the dependency on the `once_cell` crate to restore the MSRV.1.0.0 宣告 API稳定(Stabilize)。随后 1.0.1 ~ 1.2.5 几乎全是依赖维护与发布层面的操作,值得注意两点:
- docs.rs 的依赖索引问题:1.2.2、1.2.3、1.2.5 连续三次"Bump version for docs.rs to pick up latest dependencies",说明 docs.rs 的构建缓存以版本号为键,只有当版本号变化时才会重新构建、拾取最新的传递依赖。这是发布流程中一个真实且典型的运维细节。
- 1.2.1 临时降级 async-executor:当上游子 crate 出现回归时,组合层可以通过锁定/降级依赖来快速止血,这也是"分层依赖"架构在维护期的价值体现。
1.3.0 则有一个明确的工程决策:移除对once_cell的依赖以恢复 MSRV。once_cell是社区生态中很常见的初始化库,但它的最新版本会抬高最低 Rust 版本要求。移除它意味着改回标准库方案。这一决策的直接后果可以在当前源码中看到:全局执行器的惰性初始化现在使用的是标准库的std::sync::OnceLock(src/spawn.rs),并且 Cargo.toml 的依赖表中已经没有任何once_cell条目。
七、2.0.0 破坏性升级与当前状态(2.0.0 ~ 2.0.2)
# Version 2.0.0 - **Breaking:** Bump subcrates to their newest major versions. (#277, #280, #281, #282, #283) - Run the `async-process` driver on the executor. (#284) # Version 2.0.1 - Add a mention to the documentation of `smol::spawn` that tasks spawned with this function don't have their destructors called when the program ends. (#312) # Version 2.0.2 - Update docs to mention `smol-macros`. (#319)2.0.0 的核心是破坏性升级(Breaking):所有子 crate 统一升到各自的最新主版本(对应 PR #277、#280、#281、#282、#283)。从当前 Cargo.toml 可以看到升级后的依赖矩阵:async-channel 2.0、async-fs 2.0、async-io 2.1、async-lock 3.0、async-net 2.0、blocking 1.3、futures-lite 2.0、async-process 2.0(非 espidf 平台)。这意味着升级到 smol 2.x 时,必须同步检查这些子 crate 主版本带来的 API 破坏。
2.0.0 的第二个改动是把async-process驱动(driver)放到执行器上运行。这一决定的好处可以从 src/spawn.rs 的实现中直接看到:
// Prevent spawning another thread by running the process driver on this thread. let ex = Executor::new(); #[cfg(not(target_os = "espidf"))] ex.spawn(async_process::driver()).detach();注释写得很清楚:把进程驱动作为普通任务挂在全局执行器上,就避免为它再单独开一个后台线程,减少了线程数量与切换开销。cfg(not(target_os = "espidf"))则表明在 espidf(ESP-IDF 嵌入式平台)上不启用进程功能。
2.0.1 在smol::spawn的文档中补充了一个重要语义提醒:通过smol::spawn派生的任务,在程序结束时不会调用其析构函数(destructors)。这一点在当前 src/spawn.rs 中也有对应说明:"Since the executor is kept around forever,dropis not called for tasks when the program exits." 原因是全局执行器被OnceLock永久持有、永不销毁。这是写生产代码时必须记住的陷阱:如果你在任务里持有需要关闭的资源(文件、连接、锁),不能指望进程退出时自动释放。
2.0.2 更新文档,指引用户使用smol-macros获得开箱即用的 async main 与多线程执行器设置。src/lib.rs 与 README.md 都说明了这一点。smol-macros是一个不引入过程宏(no proc-macro)、编译更快的方案,在仓库的示例中已有真实使用:如 examples/hyper-client.rs 与 examples/hyper-server.rs 都通过#[apply(main!)]自动注入smol::main!宏(该示例同时演示了把&Executor注入主函数、配合smol_hyper::rt运行 hyper 服务的做法):
#[apply(main!)] async fn main(ex: &Executor<'static>) -> Result<()> { // ... 用 ex.spawn(...) 派发任务 }八、从 CHANGELOG 读懂 smol 的工程哲学
纵观 0.1.0 到 2.0.2 的全部变更,可以提炼出五条贯穿始终的设计主线:
- 分层而不是重写:v0.2.0 拆分出的子 crate 生态至今保持着清晰边界——
polling管轮询、async-io管 I/O 适配、async-executor管调度、blocking管阻塞线程池。主 crate 只做再导出与全局执行器,用户也可以只依赖子 crate。这种分层让每个层的维护与发布都互不阻塞,1.2.1 的"临时降级"与 2.0.0 的"统一升主版本"正是分层架构带来的灵活操作空间。 - 正确性优先于速度:0.1.5 ~ 0.1.15 围绕丢失唤醒、死锁、公平性、超时取整展开的密集修复,说明在一个异步运行时里,"事件不丢、唤醒不丢、任务公平"是比性能更前置的根基。
- 平台语义统一:oneshot 通知全平台统一、
AtomicU64 → AtomicUsize、eventfd 引入与泄漏修复、Windows 100% CPU 修复,共同指向"一套代码在多平台有一致语义"的目标。 - 入口 API 的收敛:
run()引入又移除、block_on定型、SMOL_THREADS移除又恢复,最终收敛为"block_on+ 可选的多线程全局执行器 + 环境变量控制线程数"的极简模型。 - MSRV 是可维护性资产:1.3.0 为恢复 MSRV 移除
once_cell、Cargo.toml 注释要求与 README 的 MSRV Policy 保持同步(当前为 1.85),并约定以 Debian Stable 自带的 Rust 版本为基准。这使 smol 在工具链迭代飞快的生态里保持了可接入性。
如果你正在使用或计划使用 smol,建议按本仓库 examples 目录的示例起步,并结合 src/lib.rs 的再导出清单确认自己需要的能力入口;升级到 2.x 时务必对照 Cargo.toml 检查子 crate 主版本是否变化,并留意smol::spawn任务在进程退出时不执行析构函数的语义边界。
- 后端
【免费下载链接】smol
A small and fast async runtime for Rust
相关推荐
BullMQ Pro v4 版本解读:移除 QueueSchedulerPro 的架构演进、破坏性变更与关键修复
BullMQ Pro v4 版本解读:移除 QueueSchedulerPro 的架构演进、破坏性变更与关键修复 导读 :本文基于 docs/gitbook/b
后端消息队列任务调度uv 0.12 变更全解:读懂 CHANGELOG 中的破坏性变更、特性固化与性能演进
uv 0.12 变更全解:读懂 CHANGELOG 中的破坏性变更、特性固化与性能演进 uv 的 CHANGELOG.md https://link.gitco
包管理器开发工具CLISails 框架 CHANGELOG 全解读:从 0.10 到 1.2 的关键演进、破坏性变更与源码级验证
Sails 框架 CHANGELOG 全解读:从 0.10 到 1.2 的关键演进、破坏性变更与源码级验证 导读 Sails 是基于 Express 与 Soc
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考