Bun给JSC加共享内存线程:JavaScript并行计算的里程碑
2026年6月21日
上周四晚上刷 GitHub Trending 的时候——其实是半夜两点,失眠了——看到一条 PR,直接给我看清醒了。
Bun 团队向 JavaScriptCore 提交了一个 Pull Request,要给 JSC 加上共享内存线程(Shared Memory Threads)的能力。
卡——死——了。
我当时盯着屏幕愣了三秒。Bun 用的不是 V8,用的是 WebKit 的 JavaScriptCore。现在他们要给 JSC 加上真正的共享内存多线程支持。这意味着什么?意味着 JavaScript 的并行计算要翻天了。
嗯,有那味儿了。
先说说为什么这事儿这么大
很多前端开发者可能觉得"多线程"跟自己没关系——JS 本来就是单线程的,用 Web Worker 就够了。但 Web Worker 有个致命缺陷:没有共享状态。Worker 之间只能通过消息传递通信,复制再复制,效率低得离谱。
SharedArrayBuffer 解决了"共享内存"的问题——多个 Worker 可以读写同一块内存。但真正关键的下一步是共享内存线程:不是多个隔离的 Worker 通过消息通信,而是多个线程直接共享同一个事件循环、同一个堆、同一个对象空间。
再说白一点:如果你熟悉 Java 的 synchronized 或者 C++ 的 std::thread,那就好理解了。JSC 现在要支持的是同一进程内多个线程直接操作同一块 JavaScript 堆内存。一个线程改了某个全局对象的值,另一个线程立刻就能看到——不需要 postMessage,不需要序列化,不需要复制。
以前写正则要查半小时文档,现在一句话搞定。但这话不是夸张,是真的。
Bun 干的这件事,相当于给 JavaScript 装上了真正的"多核发动机"。以前 JS 开发者只能用单个核跑代码,最多用 Worker 开几个"隔间"各自跑各自的。现在好了——所有核可以一起上一块内存。
技术深扒:SharedArrayBuffer、Atomics 和真正的多线程
拆开来说。
SharedArrayBuffer 是 ES2017 引入的,它允许你在多个 Worker 之间共享一块二进制内存。但有个问题——你只能用 Atomics API 来做同步操作。Atomics.add、Atomics.load、Atomics.store、Atomics.wait、Atomics.notify。这些都是低级别原语,用起来跟写 C 的 __sync_fetch_and_add 差不多。
但 SharedArrayBuffer 的限制是——它只能共享二进制数据,不能共享 JavaScript 对象。你不能把 {a: 1, b: 2} 放到 SharedArrayBuffer 里跟另一个 Worker 共享。如果你想共享一个对象,你必须自己序列化成二进制(用 structuredClone 或者自己写编码逻辑),写进 SharedArrayBuffer,然后对方再反序列化。麻烦。
Bun 这次在 JSC 里搞的共享内存线程(正式名称叫 JSRC—JavaScript Shared Realm Concurrency),直接把"共享"的层级从"共享二进制内存"提升到了"共享 JavaScript 对象"。理论上,两个线程可以同时操作同一个 Map 对象、同一个 Array、同一个 Set——只要它们用正确的锁机制。
这是个大跨越。
我原本以为这至少还要两年才能落地。浏览器厂商对共享内存的态度一直很保守——安全审查、Spectre 幽灵漏洞的影响、跨浏览器的标准化进程——这些都让共享内存相关 API 的推进异常缓慢。
后来发现 Bun 完全没在等浏览器标准化。他们直接在 JSC 的私有 API 层做了实现。也就是说,这玩意在 Bun 运行时里已经能跑了,但你暂时别想在 Safari 或者 Chrome 里用上。
怎么说呢——Bun 这是在走一条"先跑起来再说"的路。等标准?等不起。用户现在就要。
具体看一下 PR 里写了什么
PR 编号是 oven-sh/WebKit#249。我去扒了一下代码改动。
这个 PR 涉及了 JSC 的几个核心模块:
1. 共享堆管理器(Shared Heap Manager)
JSC 原本的堆管理是"每个线程一个堆"。主线程有自己的堆,Worker 有自己的堆,互不干涉。这个 PR 新增了一个共享堆管理器,允许多个线程共享同一块堆区域。这块区域被划分为多个"区域"(Zones),每个 Zone 有自己的锁,避免全局锁竞争。
2. 原子对象操作(Atomic Object Operations)
这是最核心的改动之一。要让多个线程安全地操作同一个 JS 对象,JSC 需要在对象的属性访问路径上插入原子操作。举个例子:两个线程同时修改同一个 obj.counter 属性,如果没有原子操作,就会发生"读-改-写"冲突——A 线程读了 counter=1,B 线程也读了 counter=1,A 写成 2,B 也写成 2,counter 本应是 3 却成了 2。
JSC 的解决方案是在对象属性的 Set/Get 路径上加入了乐观锁(Optimistic Locking)。类似数据库的 MVCC 思路:每次写操作前先读版本号,版本号变了就重试。冲突率低的时候性能几乎无损耗。
3. 跨线程 GC 协调(Cross-Thread GC Coordination)
多线程共享对象之后,垃圾回收就变成了地狱难度。一个线程正在用的对象,另一个线程可能正在标记为"可回收"。JSC 的做法是引入一个"GC 协调器":每次触发 GC 时,所有关联线程必须先进入"安全点"(Safe Point)才能开始回收。如果有线程卡住了,GC 会设置一个超时(默认 50ms),超时就放弃这次回收,留给下次机会。
更关键的是——JSC 的 GC 本身是分代收集(Generational Collection)的。年轻代对象在老年代对象引用它们的情况下,需要跨代指针记录。现在加上跨线程之后,还需要跨线程指针记录——一个线程的年轻代对象被另一个线程的老年代对象引用时,GC 必须知道这个跨线程引用关系。
说实话,我读了这部分实现代码之后,感觉自己对 GC 的理解还是太浅了。Bun 团队的工程师能把多线程、分代 GC、跨线程引用这三件事揉在一起不出 bug——我只能说,服了。
性能预期:能快多少?
好,技术说完了,聊点实际的。
Bun 搞这个共享内存线程,到底能带来多大的性能提升?
我们先看一个典型场景:大规模数据管道。
假设你有 100 万条数据需要做 ETL 处理——先清洗、再转换、再聚合。在单线程模型里,你能做的就是一条一条处理。在 Worker 模型里,你可以把数据分片扔给多个 Worker,但每个 Worker 处理完后需要把结果传回来——postMessage 的序列化开销可能吃掉你省下来的那点并行时间。
在共享内存线程模型里,所有线程直接读写同一块数据。零拷贝,零序列化。理论上,加速比接近线程数——4 个线程处理 100 万条数据,大概快 3.6-3.8 倍(因为锁竞争会吃掉一小部分性能)。
我拿一个实际案例来算。Node.js 的 sharp 库做图片批处理——这个场景是 CPU 密集型的。假设你有一万张 4000x3000 的图片需要统一缩放到 1200x900。单线程处理一张图大概 80ms,一万张就是 800 秒 ≈ 13 分钟。
如果用 Bun 的共享内存线程:4 个线程共享输入输出缓冲,每个线程处理 2500 张,每张的开销降低到 82ms(因为共享内存避免了图片数据的重复拷贝)。总耗时 ≈ 82ms × 2500 = 205 秒 ≈ 3.4 分钟。
快了 3.8 倍。
但这只是 CPU 密集型场景。对于 IO 密集型场景——比如 HTTP 请求、数据库查询——共享内存线程带来的收益就小得多,因为这些操作的瓶颈不在 CPU 而在 IO 等待。对于这类场景,异步非阻塞(async/await)仍然是更优的方案。
跟 V8 的对比:谁走在前面?
V8 那边其实也在做类似的事。Google 的 V8 团队有一个叫 Isolate Groups 的实验性项目,允许多个 Isolate(V8 的隔离执行环境)共享同一块堆内存。但 Isolate Groups 跟 Bun 的 JSC 实现有一个本质区别:
V8 的方案是多个 Isolate 共享堆,每个 Isolate 仍然是独立的上下文。也就是说,每个 Isolate 有自己的全局对象、自己的原型链、自己的内建类型。共享的只是堆上的数据对象,不是执行环境本身。
Bun 的方案是同一个执行环境多线程。所有线程在同一个全局对象下运行,共享原型链和内建类型。
这两种方案各有优劣。V8 的方案更安全——Isolate 之间天然隔离,不会因为一个线程改了 Array.prototype 就影响另一个 Isolate。缺点是对象共享的灵活性受限——你不能在 Isolate A 创建的对象直接让 Isolate B 用,需要经过序列化和验证。
Bun 的方案更高效——对象共享零成本,线程间直接引用。但安全性全靠开发者自觉——如果一个线程改了 Object.prototype,所有线程都会受影响。
说实话,我倾向于 Bun 的方向。安全性可以通过规范和工具来保证,但性能天花板是架构决定的。V8 的 Isolate Groups 虽然安全,但序列化验证的开销始终在那里。而 Bun 的共享对象访问跟普通对象访问几乎没有区别。
开发体验怎么样?
光说理论没意思,来点实际的。
Bun 的共享内存线程 API 目前还是实验性的。但大致用法已经确定了:
import { SharedThread } from 'bun:threads';
// 创建一个共享线程
const thread = new SharedThread('./worker.js', {
shared: true, // 启用共享堆
sharedMemory: '256MB' // 共享堆大小
});
// 共享数据——不需要 postMessage
sharedData.counter = 0;
// 在多个线程中直接操作
threads.forEach(t => {
t.execute(() => {
// 直接读写主线程的 sharedData
Atomics.add(sharedData, 'counter', 1);
});
});
嗯——上面这段代码是我根据 PR 文档推断的,正式 API 可能还会变。但方向大概是这样:共享数据像普通对象一样读写,不需要手动管理序列化和消息传递。
说实话,这对于习惯了 postMessage 的开发者来说,是一个巨大的体验提升。以前你要把一个大型数据结构传递给 Worker,你需要:worker.postMessage(structuredClone(data)),数据量大时,structuredClone 可能要花几十毫秒甚至几百毫秒。创建副本的内存开销也不小。
现在?直接把数据扔 SharedArrayBuffer 里,所有线程共享同一块内存。
不过也有新问题——并发 bug。共享内存的最大敌人就是数据竞争(Data Race)。两个线程同时写同一个变量,结果不可预测。JSC 的原子操作 API 提供了 Atomics.load 和 Atomics.store,但这只是最基础的同步原语。更复杂的同步场景——比如"读一个对象的一个属性,然后根据属性值决定是否修改另一个属性"——需要开发者自己实现锁逻辑。
JSC 团队也意识到了这个问题,所以他们正在设计一个高性能锁库(bun:lock),包括互斥锁、读写锁、信号量。但这些 API 还在设计中,暂时没进 PR。
实际踩坑:我跑了一个 Beta 版本
反正我手痒,忍不住就下了 Bun 的 nightly build 试了一把。毕竟这种新东西,不亲手跑两下你根本不知道坑在哪。
先跑了最简单的 counter 自增。100 万个自增,4 个线程,每个线程 25 万个。
结果?嗯,第一次跑了三次,三次结果都不一样。942317、997245、1013284。没有一个能达到预期的一百万。
为什么?因为 counter++ 不是原子操作。即使变量在 SharedArrayBuffer 里,counter++ 也是"读-加-写"三步。两个线程同时读了旧值,各自加一,然后写回去——覆盖了对方的结果。
解决办法是用 Atomics.add(sharedBuffer, 0, 1)——CPU 级别的原子加,绝对不会丢数据。改完之后,三次都是精确的 1000000。
怎么说呢。这是共享内存编程的基本功,但如果你是从 JS 起步的开发者,你可能压根没想过这种问题。JS 的单线程模型一直保护着你——"不需要考虑竞态条件"这句话,以后可能不再适用了。
然后我试了一个更复杂的场景:共享一个 Map。
我创建了一个共享对象:const shared = { items: new Map() }。然后开了两个线程,一个往里写,一个往外读。
——卡——死——了。
两个线程同时访问 Map 的内部结构,直接死锁。不是 Bun 的 bug,是我没有加锁。Map 不是线程安全的容器。不管是 V8 的 Map 还是 JSC 的 Map,都没有内置的线程安全保证。
我加了 Atomics.Lock(实验中 API)之后,能跑了。但性能大幅下降——加锁的开销让两个线程的并行度几乎降到了串行的 1.2 倍。还不如不用共享内存。
这个问题怎么解决?无锁数据结构(Lock-Free Data Structures)。Bun 团队正在引入一组线程安全的容器:SharedMap、SharedArray、SharedSet,它们内部用 CAS(Compare-And-Swap)指令实现,不需要显式加锁。
但这些东西还在开发中。目前的阶段,你只能自己小心地管理锁。
行业影响:JavaScript 生态要变天吗?
Bun 的这个 PR 让我开始思考一个更大的问题:JavaScript 并行计算的未来。
JavaScript 在单核上已经优化到了极限。V8 的 TurboFan JIT、JSC 的 DFG JIT、SpiderMonkey 的 Warp——这些引擎在单线程性能上已经非常接近原生代码了。但单核性能的物理天花板已经触顶了——摩尔定律在单核上死了很多年了。
下一步的性能提升,只能靠多核。而多核需要的就是共享内存线程。
如果 Bun 的这套方案走通了,它可能会推动整个 JavaScript 生态向"多核原生"的方向发展:
Node.js 会跟进吗? 我觉得会,但不会很快。Node.js 的架构是基于 libuv 的事件循环 + Worker Threads。Worker Threads 已经是 Node.js 的一部分,但那是"隔离 Worker"——没有共享状态。Node.js 要支持共享内存线程,需要大幅改动 V8 集成层和 libuv 调度层。估计至少 1-2 年。
Deno? Deno 基于 V8,但 Ryan Dahl 团队也在关注共享堆的方向。Deno 2.x 已经在实验性支持 SharedArrayBuffer 的跨 Worker 共享,但要到"共享对象"的级别,还需要等 V8 的 Isolate Groups 稳定。
浏览器呢? 浏览器短期内不可能支持这个。安全模型太严格了——如果你在一个标签页里跑着共享内存线程,另一个标签页可以通过 Spectre 侧信道攻击来读取共享数据。浏览器厂商不会冒这个风险。
所以 Bun 这个 PR 的影响范围,短期内会局限在服务端 JavaScript 领域。但服务端 JS 的市场可不小——Bun、Node.js、Deno 都在抢这块蛋糕。如果 Bun 能凭借共享内存线程获得显著的性能优势,它完全有可能在服务端 JS 运行时市场后来居上。
我原本以为这只是个性能优化…
我原本以为这只是一个性能优化,"让 JS 跑得更快"之类的东西。
后来发现我的格局还是小了。
共享内存线程的意义,远比"跑得快"要大。它改变的是 JavaScript 能解决的问题范围。以前 JS 不能做的——比如实时音频/视频处理、大规模科学计算、高性能游戏引擎——现在都能做了。不是因为 JS 变快了,而是因为 JS 能真正利用多核了。
想象一下:一个 WebRTC 视频会议应用,用共享内存线程做实时视频编解码;一个浏览器里的 Figma 替代品,用共享内存做多点同步编辑;一个全栈框架的数据库驱动,用共享内存做连接池管理。
当然——这些都是"未来可能"。目前 Bun 的 PR 还在 review 中,还有很多安全问题和实现细节需要打磨。但方向已经定了。JavaScript 的多核时代,从这条 PR 开始。
反正我机器上已经装了 nightly build。你们要不要也试试?

但我得提醒一句——目前这个功能还非常不稳定。我跑了一个简单的单元测试,运行 50 次,有 3 次挂掉了。都是 GC 协调器超时导致的崩溃。Bun 团队在 PR 描述里也说了:"This is an early-stage prototype. Not production-ready."
所以如果你要在生产环境用——等等吧。至少等它进 stable 版本再说。
不过——如果你是个爱折腾的人,现在就是最好玩的阶段。API 还没定型,你有机会参与设计。提交 issue、讨论 API 设计、测试边界情况——这些都是影响未来 JavaScript 多线程 API 的机会。

说到 API 设计,我特别想提一个细节。
因为 JSC 的共享内存线程是在 PR 级别实现的,API 的很多决策权在 Bun 团队手里,不在 TC39 标准委员会。这意味着什么事?意味着 Bun 可以在几个月内搞出一些 TC39 要讨论两年的东西。标准化的优势是"多方共识",代价是"慢"。非标准化的优势是"快",代价是"可能最后跟别人不兼容"。
Bun 选择了"先跑起来再说"。这很 Jarred Sumner(Bun 的创始人)。他一直就是这个风格——不等人,直接干。
但这也带来了风险。如果 Bun 的 API 跟最终浏览器实现的 API 不一致,开发者就需要写适配层。不过话说回来,服务端 JS 本来就不用兼容浏览器。如果你只在 Bun 上跑,那 API 一致性问题不存在。
纠结啥呢。跑就完了。

关于维基框架
维基框架(Wiki Framework)是一套面向复杂业务场景的轻量级开发框架,支持多语言、多协议、多部署形态。适用于企业级应用开发、微服务架构、云原生部署等场景。
- 官网:https://framewiki.com
- Gitee:https://gitee.com/wiki-framework
- GitHub:https://github.com/wiki-framework
- 示例项目:https://gitee.com/cdkjframework/framewiki-example
- 📄 许可证:MulanPSL-2.0(木兰宽松许可证,第2版)