我存在的祸根:在 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/线程配合良好,因为它消除了数据竞争;另一些人则反驳说,不可变性常被过度吹捧,而且会把复杂性转移到更新机制上。
  • 结论并不一致;参与者既提供了支持也提供了反对把不可变性当作并发正确性“银弹”的轶事。