- 语言运行时
- 并发编程
【免费下载链接】napajs
Napa.js: a multi-threaded JavaScript runtime
导读
transport是 Napa.js 多线程运行时中负责"跨线程传值"的核心模块。由于每个 JavaScript 线程(V8 Isolate)拥有独立堆,值在不同线程间传递必须经过 marshall/unmarshall(序列化/反序列化),本指南基于 transport API 文档,结合源码、测试与示例,系统讲解可传输类型、Constructor ID(cid)、TransportContext、函数传输与内建对象传输机制,并给出完整的 API 速查与实战案例。读完本文,你将掌握如何在 Napa zone 的多个 worker 之间高效地传递参数、共享共享内存数据,以及如何自定义可传输的 JavaScript 类。
为什么需要 transport:多 VM 之间的值传递
现有 JavaScript 引擎并非为跨多个 VM 运行而设计,每个 VM(在 Napa.js 中即一个 worker/V8 Isolate)都管理着自己的堆。要把值从一个 VM 传递到另一个 VM,就必须进行 marshall/unmarshall,而 payload 的大小和对象复杂度会直接影响通信效率。
Napa.js 的思路建立在两个事实之上:
- 所有 JavaScript VM(worker)都存在于同一个进程中;
- 原生对象可以被包装(wrap)并以 JavaScript 对象的形式暴露。
基于此,Napa 提出了一个高效对象共享的设计模式,并为此引入了如下核心概念:
- Transportable types(可传输类型):可以被透明地跨 worker 传递或共享的 JavaScript 类型;
- Constructor ID(cid):为可传输的用户类提供构造函数查找机制;
- TransportContext:携带无法序列化状态(如
std::shared_ptr、JavaScript 函数)的传输上下文。
这些概念贯穿于zone.broadcast/zone.execute的参数传递,以及store.set/store.get的键值共享中。
Transportable types:哪些值可以跨线程传输
可传输类型是 Napa.js 传输层的基础。可传输的类型包括:
| 类别 | 具体类型 |
|---|---|
| JavaScript 原始类型 | undefined、null、boolean、number、string |
实现Transportable接口的类 | TypeScript class(见下文接口定义) |
| 不引用闭包的函数 | function,详见"传输函数"一节 |
| JavaScript 标准内建对象白名单 | ArrayBuffer、SharedArrayBuffer及全部 TypedArray:Float32Array、Float64Array、Int16Array、Int32Array、Int8Array、Uint16Array、Uint32Array、Uint8Array |
| 复合结构 | 由以上类型组合而成的 Array 或普通 JavaScript 对象 |
白名单在源码中对应 lib/transport/transport.ts 中的_builtInTypeWhitelist集合,与文档列举完全一致:
let _builtInTypeWhitelist = new Set(); [ 'ArrayBuffer', 'Float32Array', 'Float64Array', 'Int16Array', 'Int32Array', 'Int8Array', 'SharedArrayBuffer', 'Uint16Array', 'Uint32Array', 'Uint8Array' ].forEach((type) => { _builtInTypeWhitelist.add(type); });关于可传输性的判定逻辑,见 lib/transport/transportable.ts 的isTransportable实现:
- 数组:递归遍历每个元素,任一元素不可传输则整体不可传输;
- 对象:若构造器名为
Object(普通对象),递归遍历每个属性;若对象提供cid()方法(即实现Transportable),则可传输;否则不可传输。
也就是说,一个未注册@cid的普通 JavaScript class 实例是不可传输的,这一点在测试 test/transport-test.ts 中得到了验证:
assert(napa.transport.isTransportable(new t.CanPass(napa.memory.crtAllocator))); assert(napa.transport.isTransportable(1)); assert(napa.transport.isTransportable('hello world')); assert(napa.transport.isTransportable([1, 2, 3])); assert(!napa.transport.isTransportable(new t.CannotPass())); assert(!napa.transport.isTransportable([1, new t.CannotPass()]));注意:普通对象判定中,对象自身可传输不要求其原型链上所有方法都可序列化——方法(函数)本身在成员位置不会被序列化(详见"传输函数"一节的限制说明)。
Constructor ID(cid):从字符串 payload 重建对象
对于实现了Transportable接口的用户类,Napa.js 使用 Constructor ID(cid)来查找构造函数,以便从字符串 payload 重建出正确类型的对象。cid会作为 payload 的一部分被序列化:在 unmarshall 过程中,传输层会提取cid,用它查找关联的构造函数,创建对象实例,然后调用该实例的unmarshall方法完成状态恢复。
cid的选择是类开发者的责任。为避免冲突,官方建议使用module.id与类名的组合作为cid。注册方式有两种:
- 类装饰器
@cid()(TypeScript 开启装饰器特性时):自动注册用户可传输类; - 手动调用
transport.register:在模块初始化阶段完成注册。
@cid装饰器的自动命名逻辑
从源码 lib/transport/transportable.ts 可以看到,@cid()无参调用时会自动提取模块名与类名拼接:
export function cid<T extends TransportableObject>(guid?: string) { let moduleName: string = null; if (!guid) { moduleName = extractModuleName(v8.currentStack(2)[1].getFileName()); } return (constructor: new(...args: any[]) => any ) => { let cid = moduleName ? `${moduleName}.${constructor.name}` : guid; (<any>constructor)['_cid'] = cid; transport.register(constructor); } }extractModuleName(lib/transport/transportable.ts)对node_modules下的模块保留相对路径,其他模块则转换为相对process.cwd()的路径,从而保证同一类在不同 worker 中得到一致的cid。若想完全掌控命名,也可以显式传 GUID:@cid('<guid>')。
transport.register的校验逻辑
源码 lib/transport/transport.ts 展示了注册时的关键校验:
export function register(subClass: new(...args: any[]) => any) { // 优先从构造函数静态属性读取 cid(针对 TransportableObject 子类) let cid: string = (<any>subClass)['_cid']; if (cid == null) { cid = new subClass().cid(); } if (cid == null) { throw new Error(`Class "${subClass.name}" doesn't implement cid(), did you forget put @cid decorator before class declaration?`); } if (_registry.has(cid)) { throw new Error(`Constructor ID (cid) "${cid}" is already registered.`); } _registry.set(cid, subClass); }该实现有两个值得注意的细节:
- 优先读取构造函数上的静态
_cid(由@cid装饰器写入),从而无需实例化对象即可拿到 cid; - 同一个
cid重复注册会直接抛错,这既是防冲突机制,也提醒开发者:cid必须是全局唯一的。
每个 Isolate 维护一份独立的cid => 构造函数注册表(见 lib/transport/transport.ts 中的_registry),因此各 worker 都需要各自注册可传输类。
unmarshall 时的 cid 查找
在 lib/transport/transport.ts 的unmarshallTransform中,payload 携带_cid字段时按如下流程重建对象:
if (payload._cid !== undefined) { let cid = payload._cid; if (cid === 'function') { return functionTransporter.load(payload.hash); // 函数传输走专门路径 } if (context == null) { throw new Error(`Cannot transport type with cid "${cid}" without a transport context.`); } let subClass = _registry.get(cid); if (subClass == null) { throw new Error(`Unrecognized Constructor ID (cid) "${cid}". Please ensure @cid is applied on the class or transport.register is called on the class.`); } let object = new subClass(); object.unmarshall(payload, context); return object; }可以看到:未注册的cid会抛出"Unrecognized Constructor ID"错误;同时依赖TransportContext的类型在无 context 时会拒绝传输。
TransportContext:承载无法序列化的共享状态
有些状态无法以序列化形式保存或加载(如std::shared_ptr),或者序列化代价过高(如 JavaScript 函数)。TransportContext 正是为这些场景引入的:
- TransportContext 对象可以被从一个 JavaScript VM 传递到另一个,也可以存放在原生世界中;
- 它延长了共享原生对象的生命周期——被保存(save)进 context 的
shared_ptr会一直存活,直到 context 被释放。
TransportContext 是 C++ addon,其实现参考napa::binding::TransportContextWrapImpl。一个典型的基于 TransportContext 实现的Transportable示例是ShareableWrap(inc/napa/module/shareable-wrap.h):它包装一个std::shared_ptr<T>,并允许其跨 isolate 共享。
从 lib/transport/transportable.ts 可以看到其 TypeScript 接口定义:
export interface TransportContext { saveShared(object: Shareable): void; loadShared(handle: Handle): Shareable; readonly sharedCount: number; }原生侧的 save/load 机制
在 inc/napa/module/shareable-wrap.h 中,SaveCallback将共享对象的指针地址以handle字段写入 payload,并调用transportContextWrap->Get()->SaveShared(thisObject->_object)保存shared_ptr;LoadCallback则从 payload 读取handle,调用LoadShared<void>(result.first)从 context 中取回同一份shared_ptr。这就是"跨 isolate 共享同一原生对象"的底层原理——传的是指针引用而非拷贝。
测试验证
test/transport-test.ts 演示了 TransportContext 的完整用法:
let tc = napa.transport.createTransportContext(); let allocator = napa.memory.debugAllocator(napa.memory.crtAllocator); tc.saveShared(allocator); // 保存 shareable 对象 shareable = tc.loadShared(allocator.handle); // 通过 handle 加载 assert.equal(shareable.refCount, 3); // sharedCount / refCount 验证对应地,通过napa.transport.createTransportContext()(见 lib/transport.ts)可以创建 context 实例。
传输函数:一次序列化,多次免费复用
JavaScript 函数是一种特殊的可传输类型。其机制是:把函数的定义(源码字符串)存入 store,目标线程根据定义重新生成一个新的函数。对应实现见 lib/transport/function-transporter.ts。
工作流程
- save(function-transporter.ts):取函数的
origin属性(默认空串)与body(func.toString()的源码),拼接后用DJB2 哈希算法(function-transporter.ts)生成 hash;函数定义({ origin, body })以hash为 key 存入惰性创建的__napajs_marshalled_functionsstore 中(见 function-transporter.ts),同时建立本 isolate 内的双向缓存。 - load(function-transporter.ts):先从本 isolate 缓存查找;未命中则从 store 取出定义,在 Node 中通过
Module._compile沙箱编译,在 Napa 中通过require(moduleId, script)编译,并给生成的函数设置origin。
在 marshall 时,函数仅在作为根对象时被传输(见 lib/transport/transport.ts):
if (typeof jsValue === 'function') { return `{"_cid": "function", "hash": "${functionTransporter.save(jsValue)}"}`; }传输函数的三个要点
- 一次成本原则:对同一个函数,marshall/unmarshall 在每个 JavaScript 线程上只发生一次。首次传输后,同一函数再次传输到同一线程可视为免费(依赖两套缓存)。
- 闭包不可传输:传输带闭包的函数不会立即报错,但在后续调用时,会得到"闭包中的变量未定义"的运行时错误。这是"传输函数不能引用闭包"这一规则的根本原因。
__dirname/__filename:在被传输的函数内可以访问这两个变量,其值由函数的origin属性决定。默认情况下origin被设置为当前工作目录。
从 docs/api/zone.md 可以看到该特性的实际应用:
zone.execute(() => { console.log(__filename);})会打印定义该函数的源文件路径。
传输 JavaScript 内建对象:ArrayBuffer / SharedArrayBuffer / TypedArray
白名单中的 JavaScript 标准内建对象可以在 Napa worker 之间透明传输;由这些类型构成的带属性对象同样可传输。底层由 V8 扩展的序列化器/反序列化器完成(JS 侧入口见 lib/transport/builtin-object-transporter.ts,其serializeValue/deserializeValue委托给require('../binding')中的原生实现)。
在 marshall 时,白名单对象会被包装为{ _serialized: serializedData }(见 lib/transport/transport.ts),unmarshall 时反向还原(lib/transport/transport.ts)。
语义差异:复制 vs 共享
这是使用内建对象传输时最需要理解的一点,测试 test/transport-test.ts 给出了明确区分:
- SharedArrayBuffer(及基于它的 TypedArray):跨线程传输时共享底层存储,多个 worker 对同一块内存的修改互相可见。测试
'@node: transport SharedArrayBuffer (SAB)'(transport-test.ts)中,4 个 worker 各自向 SAB 的不同字节写入100,主线程最终读到'100,100,100,100'。 - ArrayBuffer(及基于它的 TypedArray):传输时复制底层存储,worker 内修改不影响原始 buffer。测试
'@node: recursively transport received ArrayBuffer (AB)'(transport-test.ts)验证了这一点。
完整实战:Parallel Quick Sort
官方示例 examples/tutorial/parallel-quick-sort/parallel-quick-sort.js 演示了通过 SharedArrayBuffer 创建 TypedArray 并在多个 Napa worker 间高效共享数据的完整流程:
const napa = require("napajs"); const NUMBER_OF_WORKERS = 4; let zone = napa.zone.create('zone', { workers: NUMBER_OF_WORKERS }); // ... 定义 swap / partition / quickSort / parallelQuickSort ... function run(length) { let sab1 = new SharedArrayBuffer(length * 8); let ta1 = new Float64Array(sab1); let sab2 = new SharedArrayBuffer(length * 8); let ta2 = new Float64Array(sab2); // 以相同随机数初始化两个 TypedArray ... return zone.execute(parallelQuickSort, [ta2, 0, length - 1, parallelLength]).then(result => { // 校验排序结果 ... }); } // 通过 broadcast 把辅助函数引导到所有 worker zone.broadcast('napa = require("napajs");'); zone.broadcast(swap.toString()); zone.broadcast(partition.toString()); zone.broadcast(quickSort.toString()); zone.broadcast(parallelQuickSort.toString()); run(4 * 1024 * 1024);要点:
- 待排序的
Float64Array基于 SharedArrayBuffer,因此被zone.execute传输到任意 worker 后,worker 内对数组的写操作直接反映在主线程的同一块内存上,无需拷贝 4M 个元素,这正是高效数据共享的关键; - 辅助函数通过
zone.broadcast(codeString)预先引导到所有 worker(利用函数/代码传输机制),随后zone.execute('', 'parallelQuickSort', ...)按模块名+函数名方式调用。
注意:该测试组需要 Node.js v9.0.0 及以上版本才支持 SharedArrayBuffer 相关特性(见 transport-test.ts 的版本判断)。
完整 API 速查
isTransportable(jsValue: any): boolean
判断一个 JavaScript 值是否可传输。
// JS 原始类型均可传输 assert(transport.isTransportable(undefined)); assert(transport.isTransportable(null)); assert(transport.isTransportable(1)); assert(transport.isTransportable('string')); assert(transport.isTransportable(true)); // 可传输的 addon(如分配器) assert(transport.isTransportable(napa.memory.crtAllocator)); // 可传输类型的复合结构 assert(transport.isTransportable([ 1, "string", { a: napa.memory.crtAllocator } ])); class B { field1: number; field2: string; } // 未注册 @cid 的 JS 类不可传输 assert(!transport.isTransportable(new B()));register(transportableClass: new(...args: any[]) => any): void
在传输层能够 marshall/unmarshall 某个类的实例之前,必须先注册该类。也可以使用类装饰器@cid完成注册。
class A extends transport.AutoTransportable { field1: string, method1(): string { return this.field1; } } // 显式注册类 A transport.register(A);marshall(jsValue: any, context: TransportContext): string
将可传输的 JavaScript 值 marshall 成携带TransportContext的 JSON payload。若值不可传输则抛出 Error。
var context = transport.createTransportContext(); var jsonPayload = transport.marshall( [1, 'string', napa.memory.crtAllocator], context); console.log(jsonPayload);unmarshall(json: string, context: TransportContext): any
从 JSON payload 配合TransportContext反序列化出可传输值。若 payload 中发现cid但未在传输层注册,则抛出 Error。
var value = transport.unmarshall(jsonPayload, context);补充:marshall 内部对
undefined有特殊处理(lib/transport/transport.ts),unmarshall("undefined")直接返回undefined。
自定义可传输类:接口、抽象类与装饰器
接口Transportable
实现此接口的对象即可被传输,需要实现三个成员:
| 成员 | 签名 | 说明 |
|---|---|---|
cid | transportable.cid: string(get accessor) | 用于查找当前类 payload 对应构造函数的 Constructor ID |
marshall | transportable.marshall(context: TransportContext): object | 借助 TransportContext 将当前对象转换为普通 JavaScript 对象 |
unmarshall | transportable.unmarshall(payload: object, context: TransportContext): void | 将 marshalled 的 payload 还原为当前对象 |
抽象类TransportableObject
TransportableObject是Transportable的抽象基类(lib/transport/transportable.ts),子类需要满足三个义务:
- 构造函数接受零参数;
- 实现
save()/load()来序列化/反序列化内部状态; - 用
cid注册:通过@cid()(以'<module-name>.<class-name>'作为 cid)、@cid('<guid>')(指定 GUID)或transport.register完成注册。
基类已实现cid()、marshall()、unmarshall():
marshall(context)构建{ _cid: this.cid() }基础 payload 后调用this.save(payload, context);unmarshall(payload, context)直接委托给this.load(payload, context)。
抽象方法定义:
abstract save(payload: object, context: TransportContext): void; abstract load(payload: object, context: TransportContext): void;AutoTransportable:自动序列化基类
在 lib/transport/transportable.ts 中,AutoTransportable提供了开箱即用的save/load:
save:遍历Object.getOwnPropertyNames(this),将每个自有属性通过marshallTransform处理后写入 payload;load:遍历 payload 的自有属性,直接赋值回this。
只要类有默认构造函数、成员都是可传输类型、并通过@cid或transport.register注册,就能自动获得传输能力,无需手写序列化逻辑——上文register示例中的class A extends transport.AutoTransportable即是最佳实践。
装饰器cid
装饰器cid用于给可传输类自动注册一个 Constructor ID:
// 自动以 '<module-name>.<class-name>' 作为 cid @cid() class MyTransportable extends transport.AutoTransportable { ... } // 或显式指定 GUID @cid('a1b2c3d4-...') class AnotherTransportable extends transport.AutoTransportable { ... }与 zone / store 的协同
transport并非孤立模块,它服务于 Napa.js 的跨线程协作模型:
- 参数传递:
zone.broadcast/zone.execute的参数列表必须全部可传输(docs/api/zone.md),这正是 transport 模块的主战场; - 结果返回:
zone.execute返回的Result包含value、payload(marshalled JSON)与transportContext三个字段(docs/api/zone.md)。其中payload与transportContext的组合允许调用方在不需要还原值的情况下直接透传结果,或手动napa.transport.unmarshall(result.payload, result.transportContext)得到与result.value一致的值; - 全局存储:
store.set在写入时将值 marshall 成 JSON 存入进程堆(docs/api/store.md),所有线程都能读取,store.get时再 unmarshall。官方不推荐用 store 传递事务内/请求内的临时值,因为它有额外加锁开销,且需要开发者手动删除 key(参数传递则靠引用计数自动管理生命周期)。
需要注意的是:broadcast 中不提供 TransportContext(docs/api/zone.md),因此所有依赖 TransportContext 的类型(如ShareableWrap、Transportable)都不能出现在 broadcast 的参数列表中。
小结
Napa.js 的 transport 模块以"同进程多 isolate"为前提,设计了层次分明的跨线程传值体系:
- 类型系统:原始类型、白名单内建对象、实现
Transportable的类与不含闭包的函数,以及它们的复合结构; - 对象重建:通过
cid+ 每 isolate 注册表,从 JSON payload 精确重建对象实例,@cid装饰器与transport.register两种注册方式保证cid唯一性; - 共享语义:TransportContext 承载
shared_ptr等无法序列化的状态并延长其生命周期;SharedArrayBuffer 跨线程共享存储,ArrayBuffer 则复制存储——按需选择即可兼顾正确性与性能; - 函数传输:基于 DJB2 哈希 + store 缓存函数定义,实现"一次传输、同线程后续免费"的高效复用。
掌握这些机制后,你可以在 zone 文档 与 store 文档 的配合下,结合 transport 测试用例 和 Parallel Quick Sort 示例,构建出跨多个 worker 高效协作、数据共享透明的多线程 JavaScript 应用。
- 语言运行时
- 并发编程
【免费下载链接】napajs
Napa.js: a multi-threaded JavaScript runtime
相关推荐
Napa.js Transport API详解:对象序列化与跨线程传输
Napa.js Transport API详解:对象序列化与跨线程传输 在多线程JavaScript运行时环境中,对象的跨线程传输是实现高效协作的核心挑战。Na
语言运行时并发编程Napa.js 内建对象传输设计解析:基于 V8 序列化机制的 SharedArrayBuffer 跨 VM 共享方案
Napa.js 内建对象传输设计解析:基于 V8 序列化机制的 SharedArrayBuffer 跨 VM 共享方案 导读 Napa.js 是一个多线程 Ja
语言运行时并发编程easy-vibe 实战:用 SwiftUI 与 AI 辅助从零构建 iOS 原生应用 FridgeChef
easy vibe 实战:用 SwiftUI 与 AI 辅助从零构建 iOS 原生应用 FridgeChef 导读 本篇技术指南是 easy vibe 课程"跨
语言运行时并发编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考