【HarmonyOS开发小实践】线程间通信:序列化与 Sendable 对象
线程间通信:序列化与 Sendable 对象
Actor 模型的核心是线程间不共享内存,要交换数据只能靠消息传递。TaskPool 和 Worker 都遵循这个模型,所以只要跨线程传数据,就得过序列化这一关。序列化有性能开销,还有 16MB 大小限制。ArkTS 提供了 Sendable 对象,用共享堆实现引用传递,专门解决大数据跨线程传输的问题。这篇把序列化机制、支持的类型、Sendable 用法和两者的性能差异讲清楚。
为什么需要线程间通信
Actor 模型下每个线程有独立内存,主线程把任务交给工作线程执行,工作线程把结果返回主线程,都要传数据。比如图片处理场景,主线程把图片像素 ArrayBuffer 传给工作线程计算,工作线程算完把结果 ArrayBuffer 传回来。这中间两次跨线程传输,数据量大的话开销可观。
通信机制有两种:
- 序列化(深拷贝):把对象按结构化克隆算法序列化成字节流传过去,那边反序列化重建对象。两边对象独立,互不影响。
- 引用传递(共享内存):对象放在共享堆上,两边引用同一个对象,不拷贝。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 | 函数有闭包,无法跨线程重建 |
| Symbol | Symbol 是唯一标识,跨线程失去意义 |
| SharedArrayBuffer | 用共享内存,不走序列化 |
| WeakMap / WeakSet | 弱引用无法跨线程保持 |
| Promise | 只支持 fulfilled 状态的 Promise,结果可序列化则传递 |
| 循环引用对象 | Structured Clone 不支持循环引用 |
| 容器对象(如 ArkTS 容器) | 需用对应 Sendable 版本 |
带 @State / @Prop / @Link 装饰器的对象 | 不支持 |
序列化的开销
序列化不是免费的。对象越大、属性越多,序列化反序列化越慢。TaskPool 在任务数为 1 时的实测数据:
| 内容 | 数据量(bytes) | 序列化时间(μs) | 反序列化时间(μs) |
|---|---|---|---|
| 任务方法 | 58 | 9.549 | 43.749 |
| 参数 | 217 | 36.111 | 115.294 |
| 结果 | 47 | 16.667 | 85.243 |
数据量到 MB 级别,序列化时间会到毫秒级,跨线程传输开销就不能忽略了。再加上 16MB 单次传输上限,大数据用序列化既慢又容易撞限制。
ArrayBuffer 的特殊处理
ArrayBuffer 在 TaskPool 里默认转移所有权,传过去后宿主线程里这个 buffer 就废了:
@Concurrent
function processBuffer(buffer: ArrayBuffer): ArrayBuffer {
// 处理 buffer
return buffer;
}
const buf = new ArrayBuffer(1024 * 1024); // 1MB
const task = new taskpool.Task(processBuffer, buf);
await taskpool.execute(task);
// 这里 buf 已经失效,访问会报错
如果需要多次传同一个 ArrayBuffer,用 setCloneList() 改成拷贝传递:
const task = new taskpool.Task(processBuffer, buf);
task.setCloneList([buf]); // 改成拷贝
await taskpool.execute(task);
// buf 还能用
转移所有权比拷贝快(不用复制数据),但只能用一次。拷贝可以多次用,但有复制开销。根据场景选。
Sendable 对象
Sendable 是 ArkTS 提供的跨线程共享对象机制。Sendable 对象分配在共享堆(SharedHeap)上,所有并发实例都能访问,跨线程传递时走引用传递,不拷贝。
各个 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 示例
@Sendable
class SendableTestClass {
desc: string = 'sendable: this is SendableTestClass';
num: number = 5;
printName() {
console.info(`sendable: desc is: ${this.desc}`);
}
get getNum(): number {
return this.num;
}
}
// 跨线程传递
@Concurrent
function processSendable(obj: SendableTestClass) {
obj.printName(); // 直接调用方法
obj.num = 10; // 修改属性,主线程那边也能看到
}
async function test() {
const obj = new SendableTestClass();
const task = new taskpool.Task(processSendable, obj);
await taskpool.execute(task);
console.info(`obj.num = ${obj.num}`); // 输出 10,引用同一个对象
}
注意 obj.num = 10 这步,工作线程修改后,主线程的 obj.num 也是 10。因为两边引用的是 SharedHeap 上的同一个对象。这是 Sendable 的优势,也是坑——多线程同时修改会有数据竞争。
Sendable function 示例
@Sendable
type SendableFuncType = () => void;
@Sendable
class TopLevelSendableClass {
num: number = 1;
printNum() {
console.info('Top level sendable class');
}
}
@Sendable
function topLevelSendableFunction() {
console.info('Top level sendable function');
}
@Sendable
function sendableTestFunction() {
const topClass = new TopLevelSendableClass(); // 顶层 Sendable class,OK
topClass.printNum();
topLevelSendableFunction(); // 顶层 Sendable function,OK
console.info('Sendable test function');
}
@Sendable
class SendableTestClass {
constructor(func: SendableFuncType) {
this.callback = func;
}
callback: SendableFuncType; // 顶层 Sendable function 类型,OK
callSendableFunc() {
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';
@Sendable
class SharedCounter {
count: number = 0;
}
const counter = new SharedCounter();
const lock = new ArkTSUtils.locks.AsyncLock();
@Concurrent
async function increment(counter: SharedCounter, lock: ArkTSUtils.locks.AsyncLock) {
await lock.lockAsync(() => {
counter.count++;
});
}
也可以用对象冻结接口把 Sendable 对象冻结为只读,避免数据竞争。
序列化 vs Sendable 性能对比
传输数据量越大,Sendable 优势越明显。官方数据:
- 传输 100KB 数据,Sendable 效率提升约 20 倍
- 传输 1MB 数据,Sendable 效率提升约 100 倍
原因:
| 维度 | 序列化(深拷贝) | Sendable(引用传递) |
|---|---|---|
| 内存占用 | 两份(主线程 + 工作线程各一份) | 一份(共享堆上) |
| 传输耗时 | 随数据量线性增长 | 引用传递,常数时间 |
| 大小限制 | 单次 16MB | 受共享堆大小限制 |
| 数据竞争 | 无(独立副本) | 有(共享对象,需加锁) |
| 修改可见性 | 一边修改另一边看不到 | 一边修改另一边立刻看到 |
| 支持类型 | Structured Clone 支持的类型 | Sendable 类型 |
小数据用序列化就行,简单直接。大数据(几百 KB 以上)用 Sendable,性能差异巨大。但 Sendable 要处理数据竞争,代码复杂度高一些。
总结一下下
Sendable class 属性必须显式初始化。 不能用 !,不能省略初值。这条规则强制你写出确定的初始状态,但也意味着不能延迟初始化。
@Sendable
class Bad {
name!: string; // 报错,必须显式初始化
}
@Sendable
class Good {
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呢
判断标准:
简单说:小数据序列化,大数据一次性传用转移,大数据两边都要用且只读用 Sendable + 冻结,大数据两边都要读写用 Sendable + 锁。根据场景选最合适的,别一刀切。
更多推荐


所有评论(0)