ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【HarmonyOS开发小实践】线程间通信:序列化与 Sendable 对象

【HarmonyOS开发小实践】线程间通信:序列化与 Sendable 对象 线程间通信序列化与 Sendable 对象Actor 模型的核心是线程间不共享内存要交换数据只能靠消息传递。TaskPool 和 Worker 都遵循这个模型所以只要跨线程传数据就得过序列化这一关。序列化有性能开销还有 16MB 大小限制。ArkTS 提供了 Sendable 对象用共享堆实现引用传递专门解决大数据跨线程传输的问题。这篇把序列化机制、支持的类型、Sendable 用法和两者的性能差异讲清楚。为什么需要线程间通信Actor 模型下每个线程有独立内存主线程把任务交给工作线程执行工作线程把结果返回主线程都要传数据。比如图片处理场景主线程把图片像素 ArrayBuffer 传给工作线程计算工作线程算完把结果 ArrayBuffer 传回来。这中间两次跨线程传输数据量大的话开销可观。序列化传输postMessage / Task 参数序列化传输postMessage / Promise 返回主线程独立内存工作线程独立内存通信机制有两种序列化深拷贝把对象按结构化克隆算法序列化成字节流传过去那边反序列化重建对象。两边对象独立互不影响。引用传递共享内存对象放在共享堆上两边引用同一个对象不拷贝。Sendable 走这条路。序列化传输机制ArkTS 用的是Structured Clone 算法和 Web 标准的postMessage序列化机制一致。算法递归遍历对象的所有属性按类型序列化成字节流传输到目标线程后反序列化重建对象。支持序列化的类型类型序列化行为boolean / number / string / bigint值拷贝null / undefined直接传递Array / TypedArray递归拷贝元素Map / Set递归拷贝键值Date拷贝时间值Error拷贝 message、stack 等ArrayBuffer默认转移所有权TaskPool/ 拷贝Worker 默认RegExp拷贝 pattern、flags普通对象字面量递归拷贝可枚举属性不支持序列化的类型类型原因Function函数有闭包无法跨线程重建SymbolSymbol 是唯一标识跨线程失去意义SharedArrayBuffer用共享内存不走序列化WeakMap / WeakSet弱引用无法跨线程保持Promise只支持 fulfilled 状态的 Promise结果可序列化则传递循环引用对象Structured Clone 不支持循环引用容器对象如 ArkTS 容器需用对应 Sendable 版本带State/Prop/Link装饰器的对象不支持序列化的开销序列化不是免费的。对象越大、属性越多序列化反序列化越慢。TaskPool 在任务数为 1 时的实测数据内容数据量bytes序列化时间μs反序列化时间μs任务方法589.54943.749参数21736.111115.294结果4716.66785.243数据量到 MB 级别序列化时间会到毫秒级跨线程传输开销就不能忽略了。再加上 16MB 单次传输上限大数据用序列化既慢又容易撞限制。ArrayBuffer 的特殊处理ArrayBuffer 在 TaskPool 里默认转移所有权传过去后宿主线程里这个 buffer 就废了ConcurrentfunctionprocessBuffer(buffer:ArrayBuffer):ArrayBuffer{// 处理 bufferreturnbuffer;}constbufnewArrayBuffer(1024*1024);// 1MBconsttasknewtaskpool.Task(processBuffer,buf);awaittaskpool.execute(task);// 这里 buf 已经失效访问会报错如果需要多次传同一个 ArrayBuffer用setCloneList()改成拷贝传递consttasknewtaskpool.Task(processBuffer,buf);task.setCloneList([buf]);// 改成拷贝awaittaskpool.execute(task);// buf 还能用转移所有权比拷贝快不用复制数据但只能用一次。拷贝可以多次用但有复制开销。根据场景选。Sendable 对象Sendable 是 ArkTS 提供的跨线程共享对象机制。Sendable 对象分配在共享堆SharedHeap上所有并发实例都能访问跨线程传递时走引用传递不拷贝。进程工作线程2工作线程1主线程可引用可引用可引用LocalHeap工作线程2独占LocalHeap主线程独占SharedHeap 共享堆进程级所有线程可访问LocalHeap工作线程1独占各个 LocalHeap 互相隔离SharedHeap 是进程级共享。Sendable 对象在 SharedHeap 上分配所以多个线程能引用同一个对象。注意 SharedHeap 不能引用 LocalHeap 里的对象否则会破坏隔离。Sendable 装饰器Sendable装饰器用来声明并校验 Sendable class 和 Sendable function。规则比较严格维度规则装饰器参数无使用场景仅 Stage 模型.ets文件装饰的函数类型仅普通 function 和 async function类继承限制Sendable class 只能继承 Sendable class属性类型限制string / number / boolean / bigint / null / undefined / const enum / Sendable class / collections 容器 / AsyncLock / SendableLruCache / ConditionVariable / 自定义 Sendable 函数闭包限制禁止闭包变量顶层定义的 Sendable class 和 function 除外属性定义必须显式初始化不能用!不能用#私有不支持计算属性不支持类型别名属性修改不支持增加/删除属性允许修改属性值类型必须一致不支持修改方法Sendable class 示例SendableclassSendableTestClass{desc:stringsendable: this is SendableTestClass;num:number5;printName(){console.info(sendable: desc is:${this.desc});}getgetNum():number{returnthis.num;}}// 跨线程传递ConcurrentfunctionprocessSendable(obj:SendableTestClass){obj.printName();// 直接调用方法obj.num10;// 修改属性主线程那边也能看到}asyncfunctiontest(){constobjnewSendableTestClass();consttasknewtaskpool.Task(processSendable,obj);awaittaskpool.execute(task);console.info(obj.num ${obj.num});// 输出 10引用同一个对象}注意obj.num 10这步工作线程修改后主线程的obj.num也是 10。因为两边引用的是 SharedHeap 上的同一个对象。这是 Sendable 的优势也是坑——多线程同时修改会有数据竞争。Sendable function 示例SendabletypeSendableFuncType()void;SendableclassTopLevelSendableClass{num:number1;printNum(){console.info(Top level sendable class);}}SendablefunctiontopLevelSendableFunction(){console.info(Top level sendable function);}SendablefunctionsendableTestFunction(){consttopClassnewTopLevelSendableClass();// 顶层 Sendable classOKtopClass.printNum();topLevelSendableFunction();// 顶层 Sendable functionOKconsole.info(Sendable test function);}SendableclassSendableTestClass{constructor(func:SendableFuncType){this.callbackfunc;}callback:SendableFuncType;// 顶层 Sendable function 类型OKcallSendableFunc(){sendableTestFunction();// 顶层 Sendable functionOK}}Sendable function 的闭包限制和Concurrent类似不能用闭包变量但顶层定义的 Sendable class 和 function 可以用。Sendable 支持的数据类型基本类型boolean / number / string / bigint / null / undefinedconst enum常量枚举ArkTS 容器类型arkts.collections如 SendableMap、SendableArray异步锁对象arkts.utils.AsyncLock异步等待对象arkts.utils.ConditionVariableSendableLruCache 对象继承 ISendable 的 interfaceSendable装饰的 class 和 function接入 Sendable 的系统对象共享用户首选项、色彩管理、图片处理、资源管理、SendableContext 等元素均为 Sendable 类型的 union type自定义 Native Sendable 对象注意JS 内置对象不是 Sendable 类型。比如new Map()走的是 Structured Clone跨线程是拷贝。要用 Sendable 版本得显式引入arkts.collections。对象字面量和数组字面量也不是 Sendable跨线程也是拷贝。数据竞争和异步锁多个线程同时修改 Sendable 对象会数据竞争。ArkTS 提供异步锁AsyncLock解决import{ArkTSUtils}fromarkts.utils;SendableclassSharedCounter{count:number0;}constcounternewSharedCounter();constlocknewArkTSUtils.locks.AsyncLock();Concurrentasyncfunctionincrement(counter:SharedCounter,lock:ArkTSUtils.locks.AsyncLock){awaitlock.lockAsync((){counter.count;});}也可以用对象冻结接口把 Sendable 对象冻结为只读避免数据竞争。序列化 vs Sendable 性能对比传输数据量越大Sendable 优势越明显。官方数据传输 100KB 数据Sendable 效率提升约 20 倍传输 1MB 数据Sendable 效率提升约 100 倍原因维度序列化深拷贝Sendable引用传递内存占用两份主线程 工作线程各一份一份共享堆上传输耗时随数据量线性增长引用传递常数时间大小限制单次 16MB受共享堆大小限制数据竞争无独立副本有共享对象需加锁修改可见性一边修改另一边看不到一边修改另一边立刻看到支持类型Structured Clone 支持的类型Sendable 类型小数据用序列化就行简单直接。大数据几百 KB 以上用 Sendable性能差异巨大。但 Sendable 要处理数据竞争代码复杂度高一些。总结一下下Sendable class 属性必须显式初始化。不能用!不能省略初值。这条规则强制你写出确定的初始状态但也意味着不能延迟初始化。SendableclassBad{name!:string;// 报错必须显式初始化}SendableclassGood{name:string;// OK}Sendable class 不能用#私有属性。用private关键字代替。Sendable 不支持增加/删除属性。对象结构在创建时固定只能改值不能改结构。动态加属性会报错。Sendable 方法不能改。只能修改属性值不能替换方法。如果需要多态考虑用不同的 Sendable class。JS 内置对象不是 Sendable。new Map()/new Set()/{}/[]这些跨线程都是拷贝。要共享用arkts.collections的 Sendable 版本。Sendable 跨线程修改要加锁。不加锁多线程同时改一个 Sendable 对象会数据竞争结果不可预期。用 AsyncLock 或者把对象冻结成只读。Promise 不能跨线程传递。TaskPool 返回 pending 或 rejected 状态的 Promise 会失败只支持 fulfilled 状态。Sendable 也不包 Promise。16MB 限制是单次序列化的。不是总数据量。多次 postMessage 每次都能传 16MB。但单次超 16MB 会报错这种场景必须用 Sendable 或 ArrayBuffer 转移。ArrayBuffer 转移 vs Sendable。ArrayBuffer 转移所有权setTransferList也是避免拷贝的方案但转移后宿主线程里 buffer 失效。如果要两边都能访问用 Sendable 共享。如果只传一次转移更轻量。别把所有对象都改成 Sendable。Sendable 的约束多属性类型限制、不能动态加属性、不能改方法写起来不灵活。小对象用序列化更简单。只在数据量大、传输频繁的场景才值得改造为 Sendable。啥时候用 Sendable呢判断标准否是否是否是跨线程传数据数据量 100KB?用序列化简单直接传输后两边都要访问?用 ArrayBuffer 转移一次性传输需要修改数据?用 Sendable 冻结只读共享用 Sendable AsyncLock读写共享简单说小数据序列化大数据一次性传用转移大数据两边都要用且只读用 Sendable 冻结大数据两边都要读写用 Sendable 锁。根据场景选最合适的别一刀切。
返回列表