Bun 提交 PR 为 JavaScriptCore 添加共享内存线程支持
速览
Bun 运行时环境已提交一个公开 PR,旨在为 JavaScriptCore 引擎添加共享内存线程支持。这一改进将允许 JavaScript 代码利用多核 CPU 进行并行计算,从而显著提升高性能计算场景下的执行效率。此举标志着 JavaScript 在底层并发能力上的重要突破,有助于推动其在 AI 推理等计算密集型任务中的应用。
AI 深度解读
Bun 向 JavaScriptCore 引入共享内存线程:深度解读
背景
在 JavaScript 的并发编程历史中,Worker 一直是处理后台任务的标准方案。然而,Worker 基于消息传递(Message Passing)和结构化克隆(Structured Clone)机制,这意味着线程间的数据交换涉及昂贵的序列化、反序列化和内存拷贝过程。这种设计虽然保证了隔离性,但也带来了巨大的性能开销,特别是在处理大规模共享数据结构或需要细粒度同步的场景时。
Bun 团队近期在 GitHub 上提交了一个极具实验性的 Pull Request(PR #249),旨在为 JavaScriptCore(JSC)引擎添加“共享内存线程”(Shared-memory threads)。这一提案试图从根本上改变 JavaScript 的并发模型,从“多进程+共享内存补丁”转向真正的“多线程”模型。该功能目前处于实验阶段,尚未完全稳定,但其设计哲学和实现细节展示了 JavaScript 并发编程的未来可能性。
核心内容
该 PR 的核心目标是实现 new Thread(fn) API,允许在同一个堆(Heap)中、共享相同对象的情况下,在另一个线程上运行函数 fn。这与现有的 Worker 机制有着本质区别:没有结构化克隆,没有消息传递,也没有仅依赖 SharedArrayBuffer 的逃逸通道。在这里,共享对象就是直接共享对象本身。
1. 全新的 API 设计
Bun 提出的线程 API 极其简洁,旨在消除现有 Worker 模型的复杂性:
- 基本用法:
const t = new Thread((a, b) => { return expensive(a, b); }, x, y); t.join(); // 阻塞,返回 fn 的值或重新抛出异常 await t.asyncJoin(); // 异步版本,返回 Promise,不阻塞主线程 - 闭包支持:线程是一个闭包,它可以访问其词法作用域中的变量。因为所有线程共享同一个堆,所以它可以直接看到导入的模块、类以及周围的变量,无需像 Worker 那样将函数转换为字符串并通过
eval执行。 - 线程标识:通过
Thread.current获取当前线程,通过t.id获取引擎线程 ID(主线程为 0)。
2. 与现有 Worker 模式的对比
原文详细对比了传统 Worker 模式与新 Thread 模式在处理常见并发任务时的差异:
-
并行 Map 操作:
- Worker 模式:需要将输入数据分块,通过消息发送给多个 Worker,每个 Worker 处理完后返回结果数组,主线程再合并结果。这涉及大量的数据拷贝和消息路由。
- Thread 模式:可以直接在一个共享数组上进行原子操作(
Atomics.add)来分配任务索引,并直接写入共享的结果数组。无需分块、无需重组、无需传输。
const items = loadItems(); const results = new Array(items.length); const next = { i: 0 }; const workers = Array.from({ length: 8 }, () => new Thread(() => { for (;;) { const i = Atomics.add(next, "i", 1); if (i >= items.length) return; results[i] = transform(items[i]); // 直接写入共享数组 } })); workers.forEach(t => t.join()); -
共享缓存:
- Worker 模式:每个 Worker 拥有独立的缓存,导致重复计算;或者需要一个专门的“缓存服务器” Worker 通过
postMessage进行通信,或者手动在SharedArrayBuffer上实现哈希表和字符串驻留。 - Thread 模式:所有线程共享同一个
Map实例。配合Lock对象,可以轻松实现真正的共享缓存,每个键只计算一次。
- Worker 模式:每个 Worker 拥有独立的缓存,导致重复计算;或者需要一个专门的“缓存服务器” Worker 通过
-
取消机制:
- Worker 模式:通常使用终止协议或
worker.terminate(),但这无法返回部分结果,且可能导致资源泄漏。 - Thread 模式:通过检查一个布尔标志位(如
ctl.stop)来实现优雅取消。线程读取的是实时对象属性,一旦主线程设置标志,其他线程即可感知并退出。
- Worker 模式:通常使用终止协议或
-
实时进度反馈:
- Worker 模式:需要发送大量的
postMessage({type: "progress", ...})事件,容易淹没消息通道,需要复杂的节流和事件监听管理。 - Thread 模式:直接读取共享对象上的属性(如
progress.done),无需消息协议,无速率限制问题。
- Worker 模式:需要发送大量的
-
阻塞式交接:
- Worker 模式:Worker 无法阻塞等待数据,通常使用
SharedArrayBuffer配合Atomics.wait,但这只能传递整数索引,有效载荷需要复杂的序列化方案。 - Thread 模式:支持标准的条件变量(Condition Variable)握手。可以使用
Lock和Condition对象,让消费线程阻塞等待,直到生产线程将真正的 JS 对象放入“邮箱”并通知。
- Worker 模式:Worker 无法阻塞等待数据,通常使用
3. 运行时环境的一致性
- 单一全局对象:生成的线程运行在同一个 Realm 中。
globalThis、Array、Object.prototype、Polyfills、框架单例等,每个只存在一份实例。x instanceof Foo在所有线程中均为真。 - 模块图共享:Worker 模式下,每个 Worker 都会重新获取、解析、编译和执行所有传递依赖的模块,导致模块级别的副作用(如注册表、连接建立)重复执行。Thread 模式下,线程共享已执行的模块图,就像共享堆中的其他对象一样。
- 性能开销:生成第 8 个线程的成本仅相当于线程本身的开销(约 150KB–1MB 活跃内存,30–50KB 驻留内存),而不是复制整个应用程序的启动状态。
4. 完整的同步原语
Bun 提供了完整的同步工具集,直接映射到 JavaScript 对象:
- Lock:非递归锁,支持
hold()(快速路径 tryLock,finally 等效释放)和asyncHold()。 - Condition:条件变量,支持
wait()(原子释放+阻塞)和notify()/notifyAll()。 - ThreadLocal:线程局部存储,
.value属性对每个线程独立。 - 扩展的 Atomics:
Atomics.*操作不再局限于 TypedArray,而是扩展到普通对象属性。支持load,store,add/sub/and/or/xor,exchange,compareExchange(使用 SameValueZero 比较,支持 NaN CAS 循环),wait,waitAsync,notify。每个操作都是对自有数据属性的 SeqCst(顺序一致性)原子步骤。 - 线程限制:
Thread.restrict(obj)可以将对象绑定到调用线程,其他线程访问时会抛出ConcurrentAccessError。
关键要点
- 真正的多线程模型:Bun 的提案将 JavaScript 从“多进程+共享内存补丁”转变为真正的“多线程”模型。所有并发原语(线程池、细粒度锁、条件变量、无锁计数器)都可以直接翻译使用,无需经过序列化边界。
- 零拷贝与零序列化:通过共享堆,线程间可以直接共享对象引用,消除了结构化克隆和消息传递带来的性能瓶颈。
- 闭包即线程:线程函数是真实的闭包,可以直接访问外部变量、导入模块和类定义,无需将代码转换为字符串。
- 异常处理一致:线程中的异常可以直接通过
join()重新抛出,保留真实的异常对象和堆栈信息,而不是像 Worker 那样作为ErrorEvent传递。 - 实验性状态:目前该功能仍处于实验阶段,尚未合并。需要完成线程安全器(thread-sanitizer)清理、模糊测试(fuzzing)、基准测试优化以及长期稳定性测试。
- 向后兼容:现有的锁定回退模式和禁用线程的配置保持不变且经过验证,确保现有代码不受影响。
意义与影响
这一提案
