拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
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;}constbuf=newArrayBuffer(1024*1024);// 1MBconsttask=newtaskpool.Task(processBuffer,buf);awaittaskpool.execute(task);// 这里 buf 已经失效,访问会报错

如果需要多次传同一个 ArrayBuffer,用setCloneList()改成拷贝传递:

consttask=newtaskpool.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:string='sendable: this is SendableTestClass';num:number=5;printName(){console.info(`sendable: desc is:${this.desc}`);}getgetNum():number{returnthis.num;}}// 跨线程传递@ConcurrentfunctionprocessSendable(obj:SendableTestClass){obj.printName();// 直接调用方法obj.num=10;// 修改属性,主线程那边也能看到}asyncfunctiontest(){constobj=newSendableTestClass();consttask=newtaskpool.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:number=1;printNum(){console.info('Top level sendable class');}}@SendablefunctiontopLevelSendableFunction(){console.info('Top level sendable function');}@SendablefunctionsendableTestFunction(){consttopClass=newTopLevelSendableClass();// 顶层 Sendable class,OKtopClass.printNum();topLevelSendableFunction();// 顶层 Sendable function,OKconsole.info('Sendable test function');}@SendableclassSendableTestClass{constructor(func:SendableFuncType){this.callback=func;}callback:SendableFuncType;// 顶层 Sendable function 类型,OKcallSendableFunc(){sendableTestFunction();// 顶层 Sendable function,OK}}

Sendable function 的闭包限制和@Concurrent类似:不能用闭包变量,但顶层定义的 Sendable class 和 function 可以用。

Sendable 支持的数据类型

  • 基本类型:boolean / number / string / bigint / null / undefined
  • const enum(常量枚举)
  • ArkTS 容器类型(@arkts.collections,如 SendableMap、SendableArray)
  • 异步锁对象(@arkts.utils.AsyncLock)
  • 异步等待对象(@arkts.utils.ConditionVariable)
  • SendableLruCache 对象
  • 继承 ISendable 的 interface
  • @Sendable装饰的 class 和 function
  • 接入 Sendable 的系统对象(共享用户首选项、色彩管理、图片处理、资源管理、SendableContext 等)
  • 元素均为 Sendable 类型的 union type
  • 自定义 Native Sendable 对象

注意:JS 内置对象不是 Sendable 类型。比如new Map()走的是 Structured Clone,跨线程是拷贝。要用 Sendable 版本得显式引入@arkts.collections。对象字面量和数组字面量也不是 Sendable,跨线程也是拷贝。

数据竞争和异步锁

多个线程同时修改 Sendable 对象会数据竞争。ArkTS 提供异步锁(AsyncLock)解决:

import{ArkTSUtils}from'@arkts.utils';@SendableclassSharedCounter{count:number=0;}constcounter=newSharedCounter();constlock=newArkTSUtils.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 + 锁。根据场景选最合适的,别一刀切。

返回列表