ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Napa.js transport 传输层详解:跨线程 JavaScript 值的序列化与共享

Napa.js transport 传输层详解:跨线程 JavaScript 值的序列化与共享 语言运行时并发编程【免费下载链接】napajsNapa.js: a multi-threaded JavaScript runtime项目地址https://gitcode.com/gh_mirrors/na/napajs点击查看免费下载导读transport是 Napa.js 多线程运行时中负责跨线程传值的核心模块。由于每个 JavaScript 线程V8 Isolate拥有独立堆值在不同线程间传递必须经过 marshall/unmarshall序列化/反序列化本指南基于 transport API 文档结合源码、测试与示例系统讲解可传输类型、Constructor IDcid、TransportContext、函数传输与内建对象传输机制并给出完整的 API 速查与实战案例。读完本文你将掌握如何在 Napa zone 的多个 worker 之间高效地传递参数、共享共享内存数据以及如何自定义可传输的 JavaScript 类。为什么需要 transport多 VM 之间的值传递现有 JavaScript 引擎并非为跨多个 VM 运行而设计每个 VM在 Napa.js 中即一个 worker/V8 Isolate都管理着自己的堆。要把值从一个 VM 传递到另一个 VM就必须进行 marshall/unmarshall而 payload 的大小和对象复杂度会直接影响通信效率。Napa.js 的思路建立在两个事实之上所有 JavaScript VMworker都存在于同一个进程中原生对象可以被包装wrap并以 JavaScript 对象的形式暴露。基于此Napa 提出了一个高效对象共享的设计模式并为此引入了如下核心概念Transportable types可传输类型可以被透明地跨 worker 传递或共享的 JavaScript 类型Constructor IDcid为可传输的用户类提供构造函数查找机制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及全部 TypedArrayFloat32Array、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 IDcid从字符串 payload 重建对象对于实现了Transportable接口的用户类Napa.js 使用 Constructor IDcid来查找构造函数以便从字符串 payload 重建出正确类型的对象。cid会作为 payload 的一部分被序列化在 unmarshall 过程中传输层会提取cid用它查找关联的构造函数创建对象实例然后调用该实例的unmarshall方法完成状态恢复。cid的选择是类开发者的责任。为避免冲突官方建议使用module.id与类名的组合作为cid。注册方式有两种类装饰器cid()TypeScript 开启装饰器特性时自动注册用户可传输类手动调用transport.register在模块初始化阶段完成注册。cid装饰器的自动命名逻辑从源码 lib/transport/transportable.ts 可以看到cid()无参调用时会自动提取模块名与类名拼接export function cidT 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; (anyconstructor)[_cid] cid; transport.register(constructor); } }extractModuleNamelib/transport/transportable.ts对node_modules下的模块保留相对路径其他模块则转换为相对process.cwd()的路径从而保证同一类在不同 worker 中得到一致的cid。若想完全掌控命名也可以显式传 GUIDcid(guid)。transport.register的校验逻辑源码 lib/transport/transport.ts 展示了注册时的关键校验export function register(subClass: new(...args: any[]) any) { // 优先从构造函数静态属性读取 cid针对 TransportableObject 子类 let cid: string (anysubClass)[_cid]; if (cid null) { cid new subClass().cid(); } if (cid null) { throw new Error(Class ${subClass.name} doesnt 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示例是ShareableWrapinc/napa/module/shareable-wrap.h它包装一个std::shared_ptrT并允许其跨 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_ptrLoadCallback则从 payload 读取handle调用LoadSharedvoid(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。工作流程savefunction-transporter.ts取函数的origin属性默认空串与bodyfunc.toString()的源码拼接后用DJB2 哈希算法function-transporter.ts生成 hash函数定义{ origin, body }以hash为 key 存入惰性创建的__napajs_marshalled_functionsstore 中见 function-transporter.ts同时建立本 isolate 内的双向缓存。loadfunction-transporter.ts先从本 isolate 缓存查找未命中则从 store 取出定义在 Node 中通过Module._compile沙箱编译在 Napa 中通过require(moduleId, script)编译并给生成的函数设置origin。在 marshall 时函数仅在作为根对象时被传输见 lib/transport/transport.tsif (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.tsunmarshall 时反向还原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.tsunmarshall(undefined)直接返回undefined。自定义可传输类接口、抽象类与装饰器接口Transportable实现此接口的对象即可被传输需要实现三个成员成员签名说明cidtransportable.cid: stringget accessor用于查找当前类 payload 对应构造函数的 Constructor IDmarshalltransportable.marshall(context: TransportContext): object借助 TransportContext 将当前对象转换为普通 JavaScript 对象unmarshalltransportable.unmarshall(payload: object, context: TransportContext): void将 marshalled 的 payload 还原为当前对象抽象类TransportableObjectTransportableObject是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/loadsave遍历Object.getOwnPropertyNames(this)将每个自有属性通过marshallTransform处理后写入 payloadload遍历 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、payloadmarshalled 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 中不提供 TransportContextdocs/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 应用。赞分享语言运行时并发编程【免费下载链接】napajsNapa.js: a multi-threaded JavaScript runtime项目地址https://gitcode.com/gh_mirrors/na/napajs点击查看免费下载相关推荐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 原生应用 FridgeChefeasy vibe 实战用 SwiftUI 与 AI 辅助从零构建 iOS 原生应用 FridgeChef 导读 本篇技术指南是 easy vibe 课程跨语言运行时并发编程上一篇如何高效导出浏览器CookieGet cookies.txt LOCALLY开发者的终极实战指南下一篇终极音频解放如何快速解锁QQ音乐加密文件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表