位置:首页 > 区块链 > Tokio与Rayon陷阱:为什么Async/Await无法实现并发

Tokio与Rayon陷阱:为什么Async/Await无法实现并发

时间:2026-08-05  |  作者:宇宙开黑者  |  阅读:0

过去几年,智能合约里那种“异步调用”写法(比如 Solidity 里的 await)逐渐成了主流。因为它看起来太简单了——开发者能写出跟同步代码几乎一模一样的逻辑,感觉就像在写普通程序。

Tokio/Rayon 陷阱以及为什么 Async/Await 无法实现并发

但别被这种“熟悉”骗了。表面上的顺手,背后藏着极其复杂的结构问题。它把控制流藏起来了,让硬件真正的运行方式变得模糊,最后把调度压力全甩回开发者手里。

Vitalik Buterin 在早期讨论智能合约设计时说过类似的话:“简单”是指你看着眼熟、上手快的东西,而“简洁”是指内部结构不乱、没那么多绕弯子的地方。异步调用写起来确实“简单”,但运行起来太“复杂”了。

以太坊核心开发者曾在一场技术分享里提到这种架构上的转变:

相比轻量级线程、通道和 select 机制,异步/等待对语言和虚拟机实现者来说更省事、更轻量……但它把一部分复杂性推回给了写代码的人,常常导致“有色函数”问题——也就是你调用一个异步函数,周围所有东西都得变成异步。不过重点在于:不管你提供哪种并发模型,最好只给一种。因为同时提供多种并发实现的环境,很容易出乱子。

这些关于“多种并发实现”和异步/等待的评论,恰恰就是今天很多区块链项目在生产环境里踩坑的地方。

生产陷阱:把“异步”和“并发”搞混了

异步/等待最根本的陷阱,是它把“异步”(等待 I/O 时让出控制权)和“并发”(同时处理多个任务)混为一谈。

这个语法本身就是个陷阱。它在底层把交错的状态机伪装成了一个个独立的顺序线程。开发者被这种错觉迷惑,写一个异步函数就跟写阻塞代码一样——先调个合约接口拿数据,然后立刻处理。

但问题来了:如果数据处理涉及解析一个 10MB 的 JSON 响应、遍历一个巨大的集合,或者做一笔计算量很大的零知识证明,会发生什么?

协作式调度直接卡住了。

在像 EVM 或者 Node.js 这类协作式运行时里,线程在遇到 await 之前是不会主动让出 CPU 的。一个函数里哪怕只跑 50 毫秒的密集计算,整个线程就停住了。一瞬间,成千上万个无关的请求延迟飙升,系统变得像死了一样。与此同时,硬件资源根本没被用上。

破碎的承诺:开发者成了调度员

当这些延迟尖峰出现时,解决方案永远一模一样:把运行时拆开。用 EVM 处理链上交互,把计算密集型任务丢到专用线程池里。

最近一些项目的事后复盘,把这种痛苦暴露得很清楚。PostHog 和 Meilisearch 的工程团队都写过文章,记录在生产里解决这些复杂性的真实经历。开发者必须仔细分析每个函数,判断它属于“链上交互池”还是“计算池”,然后手动编排它们之间的消息传递。

如果开发者得手动给 I/O 和计算分区,严防死守边界避免死锁,还得用两种不同的思维模型在两个运行时之间传数据,那异步抽象就已经失败了。这个语言特性本来承诺要隐藏并发的复杂性,结果却把应用开发者变成了底层调度员。

默认无上限,默认就是 OOM

异步/等待运行时的第二种失败模式,是它们能让你毫无阻碍地无限扩容。

调用一个 spawn 操作很便宜。当下游的预言机或跨链桥在流量高峰时变慢,入口循环还在愉快地接受新连接、生成新任务。

因为异步任务和内存分配在这些生态里默认通常不设限制,系统不会主动拒绝请求。积压的任务在队列里无限堆着。应用的内存消耗一路飙升,直到操作系统内存不足(OOM)杀手直接干掉进程。

主流平台的事后分析反复指向同一个根因:队列不能解决过载问题,它只是把崩溃推迟了,而且让崩溃变得更灾难。无限容量是个谎言,默认假装无限容量是危险的。

工作窃取的神话

当系统遇到这些瓶颈,开发者往往指望更智能、能抢占、能工作窃取的调度器来均衡负载。假设是:如果一个核心空闲,它应该从繁忙的核心那里偷任务来保证公平。但在大规模场景下,公平性是吞吐量的敌人。工作窃取会破坏 CPU 缓存局部性。

当某个项目把 Erlang BEAM 虚拟机推到 100 多个核心的机器上时,系统开始卡顿。正如 Robin Morisset 详细描述的那样,试图偷取工作的空闲线程把所有 CPU 周期都花在了争抢全局 runq_lock 上——一个用于同步调度器运行队列的锁。

就算用了优化锁,把状态机搬到不同的 CPU 核心,也意味着放弃 L1 和 L2 缓存。如果每个被偷的任务都要付出 100 纳秒以上的主内存读取代价,那公平性根本不重要。如果你已经因为生产需要,被迫手动对链上交互任务和计算任务进行线程分区,那么通用的工作窃取算法早就让你失望了。你比运行时更清楚工作负载的拓扑结构。

另一种选择

我已经厌倦了异步/等待的复杂性和各种陷阱。我想要 BEAM 那种坚如磐石的容错能力,但不想忍受垃圾回收和全局工作窃取这些不透明的东西。正如 Leslie Lamport 很久以前就主张的,状态机才是并发编程的数学基础。异步/等待只是编译器的魔法,试图用很蹩脚的方式把状态机藏起来。

与其藏起状态机,为什么不把它公开,让用户拥有更好的控制原语?结果就是 Project Tina:一个固执己见、无共享、每核线程的并发框架。

Tina 用严格的约束来保证巨大的吞吐量和可靠性:

  1. 一种原语,一种心智模型。 没有 async 或 await,没有 Promise,也没有 Future。你写一个 Isolate——一个并发工作单元。处理程序就是一个标准的同步函数,对消息做出反应,然后返回 Effect。
  2. 每个核心一个线程,不共享任何东西。 Tina 把工作负载按操作系统线程分片。没有工作窃取。Isolate 从不迁移。所有跨核心通信都通过消息子系统进行。
  3. 严格限制。 内存在进程启动时预先分配。邮箱有严格上限。如果流量高峰来了,邮箱满了,调用者会立刻收到通知。系统可预测地卸掉负载,而不是让 OOM 把进程搞崩。
  4. 架构确定性。 在现代异步运行时里,任务轮询顺序和线程池调度是不透明、不确定的混乱来源。你很少确切知道你的任务会在何时何地醒来。Tina 去掉了这个。调度器是每个核心上一个严格、可见、单线程的循环。因为框架明确控制执行顺序、I/O 和时钟,系统的行为完全可预测。这解锁了 Tina 的终极超能力:确定性模拟测试(DST)。你可以在单线程上模拟网络分区或丢消息,相同的种子每次都会产生完全一样的执行顺序。

异步/等待让并发写起来容易,但让系统跑起来复杂。通过强制开发者提前显式管理状态转换、严格的内存限制和深思熟虑的架构拓扑,我用结构保证取代了运行时魔法。因为:

可预测性胜过简洁。

脚注

  1. Vitalik Buterin,“简单变得容易”,Devcon 演讲。
  2. 以太坊核心开发者,“我们做对了什么,我们做错了什么”,某技术会议。
  3. PostHog Engineering,“解开共识与计算”,posthog.com/blog。
  4. Louis Dureuill,“不要混合链上和链下逻辑”,blog.dureuill.net。
  5. Robin Morisset,“针对多核架构优化 BEAM 调度器”,Code BEAM Europe。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多