← 返回信息流
AI 资讯Hacker News·3 小时前

Bun 提交 PR 为 JavaScriptCore 添加共享内存线程支持

原标题:Bun has an open PR adding shared-memory threads to 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.terminate(),但这无法返回部分结果,且可能导致资源泄漏。
    • Thread 模式:通过检查一个布尔标志位(如 ctl.stop)来实现优雅取消。线程读取的是实时对象属性,一旦主线程设置标志,其他线程即可感知并退出。
  • 实时进度反馈

    • Worker 模式:需要发送大量的 postMessage({type: "progress", ...}) 事件,容易淹没消息通道,需要复杂的节流和事件监听管理。
    • Thread 模式:直接读取共享对象上的属性(如 progress.done),无需消息协议,无速率限制问题。
  • 阻塞式交接

    • Worker 模式:Worker 无法阻塞等待数据,通常使用 SharedArrayBuffer 配合 Atomics.wait,但这只能传递整数索引,有效载荷需要复杂的序列化方案。
    • Thread 模式:支持标准的条件变量(Condition Variable)握手。可以使用 LockCondition 对象,让消费线程阻塞等待,直到生产线程将真正的 JS 对象放入“邮箱”并通知。

3. 运行时环境的一致性

  • 单一全局对象:生成的线程运行在同一个 Realm 中。globalThisArrayObject.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 属性对每个线程独立。
  • 扩展的 AtomicsAtomics.* 操作不再局限于 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)、基准测试优化以及长期稳定性测试。
  • 向后兼容:现有的锁定回退模式和禁用线程的配置保持不变且经过验证,确保现有代码不受影响。

意义与影响

这一提案

查看原文 →github.com