【HarmonyOS】ArkTS的多线程并发(下)——线程间通信对象的传递

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装饰器来避免类实例方法的丢失

以下为大致流程

  1. 初始化数据类型
typescript 复制代码
class IPerson{
  public name: string;
  public age: number;
  constructor(name: string,age: number){
    this.name = name;
    this.age = age;
  }
  drink(){
    console.log('我TM正在喝水')
  }
}
  1. 实例化数据对象并且创建任务实例
typescript 复制代码
.onClick(() => {
  const zs = new IPerson('zhangsan',12)
  let task= new taskPool.Task(test1,zs);
  
  })
})
  1. 创建任务函数
typescript 复制代码
@Concurrent
async function test1(arg: IPerson){
  console.log('传递的taskpool:'+arg.drink())
}
  1. 使用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

  1. 实现一个处理ArrayBuffer的接口,该接口在Task中执行
  2. 通过拷贝的方式将ArrayBuffer数据传输到Task中,并处理
  3. 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异步锁

  1. 使用异步锁可以解决多线程并发实例之间数据竞争的问题,异步锁对象可能会被类对象持有
  2. 为了更方便在并发实例间获取同一锁对象,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]这是一条测试数据');
  }
}

这样即可正常传递含有方法的类对象

相关推荐
柒儿吖6 小时前
Qt for HarmonyOS 3D图片轮播组件开源鸿蒙开发实战
qt·3d·harmonyos
fuze23337 小时前
解决在虚拟机的ensp中启动路由器,卡0%且出现虚拟机卡死的方法
网络·华为·ensp
爱笑的眼睛1110 小时前
HarmonyOS分布式输入法开发:实现多设备无缝输入体验
华为·harmonyos
爱笑的眼睛1110 小时前
深入HarmonyOS打印服务:从基础到高级应用开发
华为·harmonyos
鸿蒙小白龙13 小时前
OpenHarmony内核开发实战手册:编译构建、HCK框架与性能优化
harmonyos·鸿蒙·鸿蒙系统·open harmony
穆雄雄13 小时前
Rust 程序适配 OpenHarmony 实践:以 sd 工具为例
开发语言·rust·harmonyos
╰つ栺尖篴夢ゞ17 小时前
HarmonyOS之多态样式stateStyles的使用
华为·harmonyos·statestyles·多态样式
GLAB-Mary1 天前
HCIE最优规划路线:如何系统性学习华为认证?
学习·华为·华为认证·hcie·数通
lqj_本人1 天前
鸿蒙Cordova插件架构与OnsenUI组件适配机制深度解析
华为·架构·harmonyos