1.并发间线程通信概述
线程间的通信指的是并发多线程间存在的数据交换行为
在ArkTS线程通信中,不同的数据对象的行为存在差异。例如,普通的JS对象、ArrayBuffer对象在跨线程时的行为不一致,包括序列化和反序列化、数据转移和数据共享
- 以JS对象为例,其在并发任务间的通信采用了标准的Structure Clone算法(序列化和反序列化)。该算法通过序列化将JS对象转换为与引擎无关的数据(字符串或内存块),在另一个并发实例中通过反序列化还原成与原JS对象内容一致的新对象
- ArkTS的线程间通信还支持绑定Native的JS对象的传输,以及Sendable对象的共享能力
基于ArkTS提供的TaskPool和Worker并发接口,支持多种线程间通信能力。
- 如独立的耗时任务。
- 多个耗时任务
- TaskPool与宿主线程通信,Worker线程与宿主线程通信
- Worker同步调用宿主线程的接口等
同时,基于Node-API提供的机制,C++线程可以跨线程调用ArkTs接口
2.线程间通信对象
2.1普通对象
普通对象跨线程时通过拷贝形式传递,两个线程的对象内容一致,但是指向各自线程的内存区内,被分配在各自线程的虚拟机本地堆(LocalHeap)
Object、Array、Map等对象通过这种方式实现跨并发实例通信

普通类实例对象跨线程通过拷贝形式传递,只能传递数据,类实例上的方法会丢失。可以使用@Sendable装饰器来避免类实例方法的丢失
以下为大致流程
- 初始化数据类型
typescript
class IPerson{
public name: string;
public age: number;
constructor(name: string,age: number){
this.name = name;
this.age = age;
}
drink(){
console.log('我TM正在喝水')
}
}
- 实例化数据对象并且创建任务实例
typescript
.onClick(() => {
const zs = new IPerson('zhangsan',12)
let task= new taskPool.Task(test1,zs);
})
})
- 创建任务函数
typescript
@Concurrent
async function test1(arg: IPerson){
console.log('传递的taskpool:'+arg.drink())
}
- 使用TaskPool的的execute方法执行任务
typescript
.onClick(() => {
const zs = new IPerson('zhangsan',12)
let task= new taskPool.Task(test1,zs);
taskPool.execute(task).then(()=>{
console.info("taskpool: execute task success!");
}).catch((e: BusinessError)=> {
console.error(`taskpool: execute task: Code: ${e.code}, message: ${e.message}`);
})
})
注意: 如果在任务函数调用该类的方法时会报错,提示undefine
2.2ArrayBuffer对象
ArrayBuffer由两部分组成:底层储存数据的Native内存区域以及封装操作的JS对象壳,Native部分在跨线程传递时默认为转移传递,若要实现拷贝需调用setTransferList接口
对象可被分配在虚拟机本地堆中。跨线程传递时:
- JS对象壳要经过序列化与反序列化拷贝传递
- Native内存区则有两种传递方式:拷贝和传递
ArrayBuffer的拷贝传输方式(需手动实现)
使用拷贝方式(遍历递归) ,传输后两个线程可以独立访问ArrayBuffer

- 实现一个处理ArrayBuffer的接口,该接口在Task中执行
- 通过拷贝的方式将ArrayBuffer数据传输到Task中,并处理
- UI主线程接收Task执行完毕后返回的ArrayBuffer数据
setTransferList(transfer:ArrayBuffer[]):该方法可以设置任务池中的ArrayBuffer的Transfer列表,Transfer列表中的ArrayBuffer对象在传输时不会复制Buffer的内容到工作线程,而是将Buffer的控制权进行转移,若ArrayBuffer为空,则不会进行转移,而是拷贝
typescript
import {taskpool as taskPool} from '@kit.ArkTS'
import { BusinessError } from '@kit.BasicServicesKit';
//ArrayBuffer的处理函数,需要使用@Concurrent修饰
@Concurrent
function changeImage(arrayBuffer: ArrayBuffer): ArrayBuffer{
return arrayBuffer
}
//封装任务函数
function creatImageTask(arrBuffer: ArrayBuffer, isParams: boolean): taskPool.Task{
let task: taskPool.Task = new taskPool.Task(changeImage,arrBuffer);
if(!isParams){
task.setTransferList([]);
}
return task;
}
@Entry
@Component
struct Index {
build(){
/*
*
*
*/
.onClick(() => {
let taskNum = 4;
let arrayBuffer = new ArrayBuffer(1024 * 1024);
let taskPoolGroup = new taskPool.TaskGroup();
// 创建taskNum个Task
for (let i: number = 0; i < taskNum; i++) {
let arrayBufferSlice: ArrayBuffer = arrayBuffer.slice(arrayBuffer.byteLength / taskNum * i, arrayBuffer.byteLength / taskNum * (i + 1));
// 使用拷贝方式传入ArrayBuffer,所以isParamsByTransfer为false
taskPoolGroup.addTask(creatImageTask(arrayBufferSlice, false));
}
// 执行Task
taskPool.execute(taskPoolGroup).then((data) => {
// 返回结果,对数组拼接,获得最终结果
console.log('data_result:'+ data.join(' ').toString());
}).catch((e: BusinessError) => {
console.error(e.message);
})
})
}
}
ArrayBuffer的转移传输方式
采用转移方式 的话,传输后原线程将无法使用此ArrayBuffer对象。跨线程时只需要修改封装Native内存区域操作的JS对象壳即可,无需拷贝Native内存,效率更高
应用场景:ArrayBuffer可以用来表示图片等资源,在应用开发中,处理图片(调整亮度、饱和度、大小等)时,为了避免阻塞UI主线程,可以将图片传递到子线程中处理。
2.3SharedArrayBuffer对象
SharedArrayBuffer对象内部包含一块native内存,其JS对象壳被分配在虚拟机本地堆,和ArrayBuffer一致。
支持跨并发实例间共享**(内存共享)**,但是访问及修改需要采用Atomics类,防止数据竞争。

SharedArrayBuffer和ArrayBuffer的区别

2.4Transferable对象
Transferable对象,也称为NativeBinding对象,是指绑定C++对象的JS对象,主体功能由C++提供,其JS对象被分配在本地堆(LocalHead)。跨线程传递时复用同一个C++对象,相比于JS对象的拷贝模式,传输效率更高。因此NativeBinding对象也被称为Transferable对象
共享模式
若C++实现能保证线程安全性,则NativeBinding对象的C++部分支持共享传输
跨线程传输后只需要重新创建JS壳,就可以桥接到相同的C++对象
常见的共享模式NativeBinding对象包括Context对象,它包含应用程序组件的上下文信息,提供访问系统服务和资源的方法,使应用程序组件可以与系统进行交互
转移模式
若C++无法保证线程安全性,则NativeBinding对象的C++部分需要采用转移方式传输,该模式仅共享buffer,不共享对象状态
跨线程传输后重新创建JS壳即可桥接到C++对象上,但是需要移除原对象的绑定关系。即原线程无法访问
常见的转移模式的NativeBinding对象包括PixelMap对象,他可以读取或写入图像数据,获取图像信息
为什么PixelMap对象要使用转移模式
- 避免数据竞争,当使用共享模式时必须通过锁来保证操作的原子性,PixelMap对象数据量较大,频繁加锁会导致性能瓶颈
2.6Sendable对象(重点重点重点)
2.6.1简介
Sendable对象类型是ArkTS中为了优化对象间并发通信开销而提供的,支持在并发通信时通过引用传递来优化开销
传统JS引擎中,要优化对象的并发通信开销,唯一的方法就是将实现下沉到Native侧,通过Transferable对象的转移或共享来降低开销
Sendable对象为可共享的,其跨线程前后指向同一个JS对象。如果包含JS或者Native内容则可直接共享。

2.6.2Sendable的实现原理
共享堆
为实现Sendable数据在不同并发实例间的引用传递,Sendable共享对象分配在共享堆中,实现跨并发实例的内存共享
- 共享堆是进程级别 的堆空间,与LocalHead不同的是,LocalHead只能被单个并发实例访问,而SharedHead可以被所有线程访问。
- 因此一个Sendable对象可能被多个并发实例引用,判断该Sendable对象是否存活就取决于是否有实例存在对该对象的引用
SharedHead与LocalHead之间的关系

各个并发实例的LocalHead是相互隔离的,但是SharedHead是进程级别的对,可以被所有并发实例引用,但是SharedHead不能引用LocalHead中的对象
2.6.3ISendable
ISendable是所有Sendable类型的父类型。ISendable主要用在开发者自定义Sendable数据结构的场景中。
类装饰器@Sendable是implement ISendable的语法糖
2.6.4异步锁
- 使用异步锁可以解决多线程并发实例之间数据竞争的问题,异步锁对象可能会被类对象持有
- 为了更方便在并发实例间获取同一锁对象,AsyncLock对象支持线程之间引用传递
异步锁是一种非阻塞锁中的一种
- 阻塞锁:当一个线程尝试获取一个已经被其他线程持有的锁时,会停止执行(阻塞),并一直等待,直到锁可用
- 非阻塞锁:当一个线程尝试获取一个锁时,无论锁是否可用,都会立即返回一个结果,而不会等待
- 优势
- ArkTS支持异步操作,阻塞锁容易产生死锁,因此在ArkTS中仅支持异步锁(非阻塞锁) 如果在主线程中使用阻塞锁,主线程在等待锁时将被直接阻塞,无法响应(异步具有不确定性)
- 可以保证单线程内的异步任务时序的一致性
2.6.4Sendable的使用场景
Sendable对象在不同线程之间默认采用引用传递,这种方法不会丢失类成员的方法而且比序列化高效
- 跨并发实例传输大数据(100KB以上)
- 跨并发实例传递带方法的class对象
3.跨并发实例传递带方法的class
在序列化传输实例对象时,会丢失方法,例如
typescript
class test{
public num: number = 114514;
print(){
console.log('[taskPoll]这是一条测试数据');
}
}
@Concurrent
function taskFun(obj: test){
try {
console.log(`[taskPoll]num:${obj.num} `);
obj.print();
}catch (err) {
console.log('[taskPoll]taskFun执行出错,错误信息:'+err);
}
}
async function start(){
try {
let obj: test = new test();
let task:taskpool.Task = new taskpool.Task(taskFun,obj);
await taskpool.execute(task);
}catch (err) {
console.log('[taskPoll]start执行出错,错误信息:'+err);
}
}
执行结果如下

此时我们就需要给所传输的对象的类型声明前加上@Sendable
typescript
@Sendable
class test{
public num: number = 114514;
print(){
console.log('[taskPoll]这是一条测试数据');
}
}
这样即可正常传递含有方法的类对象

