我存在的祸根:在 Rust 中同时支持 async 和 sync 代码
在 Rust 中同时支持同步和异步代码,暴露出现代编程中的一个更深层张力:一旦库采用了 async I/O,它的“颜色”往往会沿着 API 向上传播,让想要简单阻塞式接口的调用方使用起来变得复杂。评论者将 Rust 生态——多个运行时、缺少标准化 async traits,以及 feature flag 约束使双栈 sync/async 支持痛苦不堪——与 Go、JavaScript、Python 和 Zig 等语言进行对比;这些语言各自以不同方式在易用性、性能和运行时复杂度之间做出权衡。提出的变通方案包括分离 sync/async crate、`block_on` 包装器、sans-I/O 设计和 effect systems,但都无法完全消除库作者在维护和设计上的负担。
Rust 库之痛:同时支持 sync 和 async
- 核心问题:库希望同时提供 sync 和 async API,而不需要重复代码、引发 feature flag 爆炸,或者强迫 sync 用户引入 async 运行时。
- Cargo 的 feature 规则(“features 必须是可加性的”)让互斥的 sync/async 配置变得很别扭。
- 维护两个 crate(例如
-sync和-async)或两条并行代码路径都被尝试过,但都被认为很笨重。 - 用户也不喜欢因为某个依赖选择了 async,就被迫进入 async。
提议的做法
- 在专门线程里使用
block_on,在 async 核心之上暴露 sync 外观;很多人认为这是一个务实的折中。 - 只保留 async API,并建议 sync 用户直接在其上阻塞,不过这仍然会“给调用者染色”。
- 为 sync 和 async 分离 crate 或模块,并且对命名约定进行了不少琐碎争论。
- Sans-IO 风格:暴露纯请求/响应类型,让调用者自己选择 sync 或 async HTTP 客户端。
- 宏 / 代码生成(如 Python 的
unasync或 Nim 里类似 “multisync” 的想法),自动生成 sync 和 async 变体。
Async vs sync:概念层面的争论
- 有些人认为 async 很“优雅”,也是表达复杂状态机和高并发 I/O 的最自然方式。
- 另一些人觉得,对很多工作负载来说,futures 和状态机不如 scoped threads 自然。
- 还有人认为,async 的好处只有在大量任务并发运行时才会显现;对于纯顺序代码,它增加了心理负担。
- 对 async 是否能避免并发 bug 也存在分歧;另一些人指出,一旦在原本直线式的代码中插入
await,竞态条件仍然可能出现。
跨语言对比
- JS:async 集成得很顺,因为 I/O 一直都是基于回调和单线程的;不过把 async 隐藏在 sync 之下仍然很难。
- Python:async 开启了新的模式,但
asyncio被批评为沉重,而且有时比简单线程还慢。 - Go:因其单一的“绿色”并发模型而受到称赞;争论在于它到底是真的避免了 function coloring,还是只是标准化了某一种“颜色”。
- Haskell:green threads 和
async被认为在易用性上更好;不可变性有助于理解并发。
Function coloring 与运行时
- “Function coloring” 被广泛视为底层痛点:async 标注会像病毒一样沿着调用栈和跨模块传播。
- Rust 缺少内置运行时,以及 Tokio/其他运行时之间的碎片化,加剧了摩擦(async traits、运行时耦合、取消语义)。
- 有人希望未来的解决方案(keyword generics、effect systems、async-generic 设计)能减少重复,并让代码与运行时无关。
不可变性、并发与 bug
- 其中一个分支讨论认为,不可变性与 async/线程配合良好,因为它消除了数据竞争;另一些人则反驳说,不可变性常被过度吹捧,而且会把复杂性转移到更新机制上。
- 结论并不一致;参与者既提供了支持也提供了反对把不可变性当作并发正确性“银弹”的轶事。