鸿蒙多线程并发

一、简介

并发是指在同一时间内,存在多个任务同时执行的情况。对于多核设备,这些任务可能同时在不同CPU上并行执行。为了提升应用的响应速度与帧率,避免耗时任务对UI主线程的影响,鸿蒙提供了异步并发和多线程并发两种处理策略。本文介绍的是多线程并发。

  • 异步并发是指异步代码在执行到一定程度后会被暂停,以便在未来某个时间点继续执行,这种情况下,同一时间只有一段代码在执行。鸿蒙通过Promise和async/await提供异步并发能力,适用于单次I/O任务的开发场景。
  • 多线程并发允许在同一时间段内同时执行多段代码。在UI主线程继续响应用户操作和更新UI的同时,后台线程也能执行耗时操作,从而避免应用出现卡顿。鸿蒙通过TaskPool和Worker提供多线程并发能力,适用于耗时任务等并发场景。

二、Actor并发模型对比内存共享并发模型

  • 内存共享并发模型指线程访问内存前需要抢占并锁定内存的使用权,没有抢占到内存的线程需要等待其他线程释放使用权再执行。Java线程使用的就是内存共享并发模型。
  • Actor并发模型每一个线程都是一个独立的内存,Actor之间通过消息传递机制进行通信,不同Actor之间不能直接访问对方的内存空间。优势在于不同线程间内存隔离,不会产生不同线程竞争同一内存资源的问题,不需要考虑对内存上锁导致的一系列功能、性能问题,提升了开发效率。

三、TaskPool

并发函数使用@Concurrent,调用taskpool.execute执行并发函数。如下代码,add函数就是在子线程中执行。

typescript 复制代码
import { taskpool } from '@kit.ArkTS';

@Concurrent
function add(num1: number, num2: number): number {
  return num1 + num2;
}

async function ConcurrentFunc(): Promise<void> {
  try {
    let task: taskpool.Task = new taskpool.Task(add, 1, 2);
    console.info("taskpool res is: " + await taskpool.execute(task));
  } catch (e) {
    console.error("taskpool execute error is: " + e);
  }
}

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            try {
                let task: taskpool.Task = new taskpool.Task(add, 1, 2);
                console.info("taskpool res is: " + await taskpool.execute(task));
            } catch (e) {
                console.error("taskpool execute error is: " + e);
            }
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

四、Worker

在宿主线程中通过调用ThreadWorker的constructor()方法创建Worker对象,当前线程为宿主线程,并注册回调函数。

typescript 复制代码
@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          // 创建Worker对象
          let workerInstance = new worker.ThreadWorker('entry/ets/workers/worker.ets');

          // 注册onmessage回调,当宿主线程接收到来自其创建的Worker通过workerPort.postMessage接口发送的消息时被调用,在宿主线程执行
          workerInstance.onmessage = (e: MessageEvents) => {
            let data: string = e.data;
            console.info("workerInstance onmessage is: ", data);
          }

          // 注册onerror回调,当Worker在执行过程中发生异常时被调用,在宿主线程执行
          workerInstance.onerror = (err: ErrorEvent) => {
            console.info("workerInstance onerror message is: " + err.message);
          }

          // 注册onmessageerror回调,当Worker对象接收到一条无法被序列化的消息时被调用,在宿主线程执行
          workerInstance.onmessageerror = () => {
            console.info('workerInstance onmessageerror');
          }

          // 注册onexit回调,当Worker销毁时被调用,在宿主线程执行
          workerInstance.onexit = (e: number) => {
            // 当Worker正常退出时code为0,异常退出时code为1
            console.info("workerInstance onexit code is: ", e);
          }

          // 向Worker线程发送消息
          workerInstance.postMessage('1');
        })
    }
    .height('100%')
    .width('100%')
  }
}

在Worker文件中注册回调函数

typescript 复制代码
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

// 注册onmessage回调,当Worker线程收到来自其宿主线程通过postMessage接口发送的消息时被调用,在Worker线程执行
workerPort.onmessage = (e: MessageEvents) => {
  let data: string = e.data;
  console.info('workerPort onmessage is: ', data);

  // 向主线程发送消息
  workerPort.postMessage('2');
}

// 注册onmessageerror回调,当Worker对象接收到一条无法被序列化的消息时被调用,在Worker线程执行
workerPort.onmessageerror = () => {
  console.info('workerPort onmessageerror');
}

// 注册onerror回调,当Worker在执行过程中发生异常被调用,在Worker线程执行
workerPort.onerror = (err: ErrorEvent) => {
  console.info('workerPort onerror err is: ', err.message);
}

五、TaskPool和Worker对比

  • TaskPool和Worker均支持多线程并发能力。由于TaskPool的工作线程会绑定系统的调度优先级,并且支持负载均衡(自动扩缩容),而Worker需要开发者自行创建,存在创建耗时以及不支持设置调度优先级,故在性能方面使用TaskPool会优于Worker,因此大多数场景推荐使用TaskPool。
  • 运行时间超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时)的任务。例如后台进行1小时的预测算法训练等CPU密集型任务,需要使用Worker。场景示例可参考常驻任务开发指导。
  • 有关联的一系列同步任务。例如在一些需要创建、使用句柄的场景中,句柄创建每次都是不同的,该句柄需永久保存,保证使用该句柄进行操作,需要使用Worker。场景示例可参考使用Worker处理关联的同步任务.
  • 需要设置优先级的任务。例如图库直方图绘制场景,后台计算的直方图数据会用于前台界面的显示,影响用户体验,需要高优先级处理,需要使用TaskPool。
  • 需要频繁取消的任务。例如图库大图浏览场景,为提升体验,会同时缓存当前图片左右侧各2张图片,往一侧滑动跳到下一张图片时,要取消另一侧的一个缓存任务,需要使用TaskPool。
  • 大量或者调度点较分散的任务。例如大型应用的多个模块包含多个耗时任务,不方便使用Worker去做负载管理,推荐采用TaskPool。场景示例可参考批量数据写数据库场景.

六、线程间通信

线程间通信指的是并发多线程间存在的数据交换行为。对于不同的数据对象,在ArkTS线程间通信的行为是有差异的,比如普通JS对象、ArrayBuffer对象、SharedArrayBuffer对象等,跨线程的行为是不一致的,包括序列化反序列化拷贝、数据转移、数据共享等不同行为。

6、1 普通对象

普通对象跨线程时通过拷贝形式传递,两个线程的对象内容一致,但是指向各自线程的隔离内存区间,被分配在各自线程的虚拟机本地堆(LocalHeap)。普通类实例对象跨线程通过拷贝形式传递,只能传递数据,类实例上的方法会丢失。可以使用@Sendable装饰器标识为Sendable类,类实例对象跨线程传递后,可携带类方法。

typescript 复制代码
export class TestA {
  constructor(name: string) {
    this.name = name;
  }
  name: string = 'ClassA';
}

import { taskpool } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';
import { TestA } from './Test';

@Concurrent
async function test1(arg: TestA) {
  console.info("TestA name is: " + arg.name);
}

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          // 1. 创建Test实例objA
          let objA = new TestA("TestA");
          // 2. 创建任务task,将objA传递给该任务,objA非sendable对象,通过序列化传递给子线程
          let task = new taskpool.Task(test1, objA);
          // 3. 执行任务
          taskpool.execute(task).then(() => {
            console.info("taskpool: execute task success!");
          }).catch((e:BusinessError) => {
            console.error(`taskpool: execute task: Code: ${e.code}, message: ${e.message}`);
          })
        })
    }
    .height('100%')
    .width('100%')
  }
}

6、2 ArrayBuffer

ArrayBuffer内部包含一块Native内存,该ArrayBuffer的JS对象壳被分配在虚拟机本地堆(LocalHeap)。与普通对象一样,需要经过序列化与反序列化拷贝传递,但是Native内存有两种传输方式:拷贝和转移。传输时采用拷贝的话,需要经过深拷贝(递归遍历),传输后两个线程都可以独立访问ArrayBuffer。如果采用转移的方式,则原线程无法使用此ArrayBuffer对象,跨线程时只需重建JS壳,Native内存无需拷贝,效率更高。TaskPool传递ArrayBuffer数据时,默认使用转移的方式,使用拷贝需调用setTransferList。

ArrayBuffer可以用来表示图片等资源,在应用开发中,会遇到需要进行图片处理的场景(比如需要调整一张图片的亮度、饱和度、大小等),为了避免阻塞UI主线程,可以将图片传递到子线程中执行这些操作。转移方式性能更高,但是原线程不能再访问ArrayBuffer对象,如果两个线程都需要访问,则需要采用拷贝方式,否则建议采用转移方式,提升性能。

6、3 SharedArrayBuffer

SharedArrayBuffer内部包含一块Native内存,其JS对象壳被分配在虚拟机本地堆(LocalHeap)。支持跨并发实例间共享,但是访问及修改需要采用Atomics类,防止数据竞争。SharedArrayBuffer可以用于多个并发实例间的状态共享或者数据共享。

如下代码,使用TaskPool传递一个Int32Array对象。

javascript 复制代码
import { taskpool } from '@kit.ArkTS';

@Concurrent
function transferAtomics(arg1: Int32Array) {
  console.info("wait begin::");
  // 使用Atomics进行操作
  let res = Atomics.wait(arg1, 0, 0, 3000);
  return res;
}

// 定义可共享对象
let sab: SharedArrayBuffer = new SharedArrayBuffer(20);
let int32 = new Int32Array(sab);
let task: taskpool.Task = new taskpool.Task(transferAtomics, int32);
taskpool.execute(task).then((res) => {
  console.info("this res is: " + res);
});
setTimeout(() => {
  Atomics.notify(int32, 0, 1);
}, 1000);

6、4 Transferable对象(NativeBinding对象)

NativeBinding对象指的是一个JS对象,绑定了一个C++对象,且主体功能由C++提供,其JS对象壳被分配在虚拟机本地堆(LocalHeap)。

如果C++实现能够保证线程安全性,则这个NativeBinding对象的C++部分可以支持共享传输。此时,NativeBinding对象跨线程传输后,只需要重新创建JS壳,就可以桥接到相同的C++对象上。常见的共享模式NativeBinding对象包括Context。

如果C++实现包含了数据,且无法保证线程安全性,则这个NativeBinding对象的C++部分需要采用转移方式传输。此时,NativeBinding对象跨线程传输后,只需要重新创建JS壳,就可以桥接到C++对象上,不过原对象需要移除对此对象的绑定关系。

6、5 Sendable对象

当需要跨线程传递大对象,拷贝影响性能。Sendable对象类型在并发通信时使用引用传递。引用传递的好处是可以跨线程传递大对象,还可以传递带方法的对象。

6、5、1 Sendable的实现原理

Sendable共享对象分配在共享堆中,共享堆(SharedHeap)是进程级别的堆空间,与虚拟机本地堆(LocalHeap)不同的是,LocalHeap只能被单个并发实例访问,而SharedHeap可以被所有线程访问。Sendable对象为可共享的,其跨线程前后指向同一个JS对象,如果其包含了JS或者Native内容,均可以直接共享,如果底层是Native实现的,则需要考虑线程安全性。

6、5、2 异步锁

当多个并发实例尝试同时更新Sendable数据时,会发生数据竞争,鸿蒙提供异步锁机制保证ArkTS安全访问。阻塞锁容易产生死锁问题,ArkTS中仅支持异步锁(非阻塞式锁)。异步锁还可以用于保证单线程内的异步任务时序一致性,防止异步任务时序不确定导致的同步问题。

typescript 复制代码
import { ArkTSUtils, taskpool } from '@kit.ArkTS';

@Sendable
export class A {
  private count_: number = 0;
  lock_: ArkTSUtils.locks.AsyncLock = new ArkTSUtils.locks.AsyncLock();

  public async getCount(): Promise<number> {
    // 对需要保护的数据加异步锁
    return this.lock_.lockAsync(() => {
      return this.count_;
    })
  }

  public async increaseCount() {
    // 对需要保护的数据加异步锁
    await this.lock_.lockAsync(() => {
      this.count_++;
    })
  }
}

@Concurrent
async function printCount(a: A) {
  console.info("InputModule: count is:" + await a.getCount());
}

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(async () => {
          // 创建sendable对象a
          let a: A = new A();
          // 将实例a传递给子线程
          await taskpool.execute(printCount, a);
        })
    }
    .height('100%')
    .width('100%')
  }
}

6、5、3 ASON解析与生成

ASON则提供了Sendable对象的序列化、反序列化能力。可以通过ASON.stringify方法将对象转换成字符串,也可以通过ASON.parse方法将字符串转成Sendable对象,以便此对象在并发任务间进行高性能引用传递。

javascript 复制代码
import { ArkTSUtils, collections } from '@kit.ArkTS';

ArkTSUtils.ASON.parse("{}")
ArkTSUtils.ASON.stringify(new collections.Array(1, 2, 3))

6、5、4 Sendable对象冻结

Sendable对象支持冻结操作,冻结后的对象变成只读对象,不能增删改属性,因此在多个并发实例间访问均不需要加锁,可以通过调用Object.freeze接口冻结对象。

typescript 复制代码
// Index.ets
import { freezeObj } from './helper';
import { worker } from '@kit.ArkTS';
 
@Sendable
export class GlobalConfig {
  // 一些配置属性与方法
  init() {
    // 初始化相关逻辑
    freezeObj(this); // 初始化完成后冻结当前对象
  }
}
 
@Entry
@Component
struct Index {
  build() {
    Column() {
      Text("Sendable freezeObj Test")
        .id('HelloWorld')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .onClick(() => {
          let gConifg = new GlobalConfig();
          gConifg.init();
          const workerInstance = new worker.ThreadWorker('entry/ets/workers/Worker.ets', { name: "Worker1" });
          workerInstance.postMessage(gConifg);
        })
    }
    .height('100%')
    .width('100%')
  }
}

6、5、5 Sendable使用场景

Sendable对象可以在不同并发实例间通过引用传递。通过引用传递方式传输对象相比序列化方式更加高效,同时不会丢失class上携带的成员方法。因此,Sendable主要可以解决两个场景的问题:

  • 跨并发实例传输大数据(例如可能达到100KB以上的数据)。
  • 跨并发实例传递带方法的类实例对象。
跨并发实例传输大数据

跨并发实例序列化的开销随着数据量线性增长,因此当传输数据量较大时(100KB数据大约1ms传输耗时),跨并发实例的拷贝开销大,影响应用性能。引用传递方式传输对象可提升性能。

typescript 复制代码
import { taskpool } from '@kit.ArkTS';
import { testTypeA, testTypeB, Test } from './sendable';
import { BusinessError, emitter } from '@kit.BasicServicesKit';
 
// 在并发函数中模拟数据处理
@Concurrent
async function taskFunc(obj: Test) {
  console.info("test task res1 is: " + obj.data1.name + " res2 is: " + obj.data2.name);
}
 
async function test() {
  // 使用taskpool传递数据
  let a: testTypeA = new testTypeA("testTypeA");
  let b: testTypeB = new testTypeB("testTypeB");
  let obj: Test = new Test(a, b);
  let task: taskpool.Task = new taskpool.Task(taskFunc, obj);
  await taskpool.execute(task);
}
 
@Concurrent
function SensorListener() {
  // 监听逻辑
  // ...
}
 
@Entry
@Component
struct Index {
  build() {
    Column() {
      Text("Listener task")
        .id('HelloWorld')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .onClick(() => {
          let sensorTask = new taskpool.LongTask(SensorListener);
          emitter.on({ eventId: 0 }, (data) => {
            // Do something here
            console.info(`Receive ACCELEROMETER data: {${data.data?.x}, ${data.data?.y}, ${data.data?.z}`);
          });
          taskpool.execute(sensorTask).then(() => {
            console.info("Add listener of ACCELEROMETER success");
          }).catch((e: BusinessError) => {
            // Process error
          })
        })
      Text("Data processing task")
        .id('HelloWorld')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .onClick(() => {
          test();
        })
    }
    .height('100%')
    .width('100%')
  }
}

// 将数据量较大的数据在Sendable class中组装
@Sendable
export class testTypeA {
  name: string = "A";
  constructor(name: string) {
    this.name = name;
  }
}

@Sendable
export class testTypeB {
  name: string = "B";
  constructor(name: string) {
    this.name = name;
  }
}

@Sendable
export class Test {
  data1: testTypeA;
  data2: testTypeB;
  constructor(arg1: testTypeA, arg2: testTypeB) {
    this.data1 = arg1;
    this.data2 = arg2;
  }
}
跨并发实例传递带方法的实例对象

序列化传输实例对象时会丢失方法,在必须调用实例方法的场景中,需使用引用传递方式进行开发。在数据处理过程中有需要解析的数据,可使用ASON工具进行数据解析。

typescript 复制代码
import { taskpool, ArkTSUtils } from '@kit.ArkTS';
import { SendableTestClass, ISendable } from './sendable';
 
// 在并发函数中模拟数据处理
@Concurrent
async function taskFunc(sendableObj: SendableTestClass) {
  console.info("SendableTestClass: name is: " + sendableObj.printName() + ", age is: " + sendableObj.printAge() + ", sex is: " + sendableObj.printSex());
  sendableObj.setAge(28);
  console.info("SendableTestClass: age is: " + sendableObj.printAge());
 
  // 解析sendableObj.arr数据生成JSON字符串
  let str = ArkTSUtils.ASON.stringify(sendableObj.arr);
  console.info("SendableTestClass: str is: " + str);
 
  // 解析该数据并生成ISendable数据
  let jsonStr = '{"name": "Alexa", "age": 23, "sex": "female"}';
  let obj = ArkTSUtils.ASON.parse(jsonStr) as ISendable;
  console.info("SendableTestClass: type is: " + typeof obj);
  console.info("SendableTestClass: name is: " + (obj as object)?.["name"]); // 输出: 'Alexa'
  console.info("SendableTestClass: age is: " + (obj as object)?.["age"]); // 输出: 23
  console.info("SendableTestClass: sex is: " + (obj as object)?.["sex"]); // 输出: 'female'
}
async function test() {
  // 使用taskpool传递数据
  let obj: SendableTestClass = new SendableTestClass();
  let task: taskpool.Task = new taskpool.Task(taskFunc, obj);
  await taskpool.execute(task);
}
 
@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
 
  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          test();
        })
    }
    .height('100%')
    .width('100%')
  }
}

// 定义模拟类Test,模仿开发过程中需传递带方法的class
import { lang, collections } from '@kit.ArkTS'

export type ISendable = lang.ISendable;

@Sendable
export class SendableTestClass {
  name: string = 'John';
  age: number = 20;
  sex: string = "man";
  arr: collections.Array<number> = new collections.Array<number>(1, 2, 3);
  constructor() {
  }
  setAge(age: number) : void {
    this.age = age;
  }

  printName(): string {
    return this.name;
  }

  printAge(): number {
    return this.age;
  }

  printSex(): string {
    return this.sex;
  }
}
相关推荐
Van_captain1 小时前
rn_for_openharmony常用组件_Breadcrumb面包屑
javascript·开源·harmonyos
御承扬1 小时前
鸿蒙原生系列之动画效果(帧动画)
c++·harmonyos·动画效果·ndk ui·鸿蒙原生
行者962 小时前
Flutter与OpenHarmony深度集成:数据导出组件的实战优化与性能提升
flutter·harmonyos·鸿蒙
小雨下雨的雨2 小时前
Flutter 框架跨平台鸿蒙开发 —— Row & Column 布局之轴线控制艺术
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨3 小时前
Flutter 框架跨平台鸿蒙开发 —— Center 控件之完美居中之道
flutter·ui·华为·harmonyos·鸿蒙
小雨下雨的雨4 小时前
Flutter 框架跨平台鸿蒙开发 —— Icon 控件之图标交互美学
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨4 小时前
Flutter 框架跨平台鸿蒙开发 —— Placeholder 控件之布局雏形美学
flutter·ui·华为·harmonyos·鸿蒙系统
行者964 小时前
OpenHarmony Flutter弹出菜单组件深度实践:从基础到高级的完整指南
flutter·harmonyos·鸿蒙
小雨下雨的雨5 小时前
Flutter 框架跨平台鸿蒙开发 —— Padding 控件之空间呼吸艺术
flutter·ui·华为·harmonyos·鸿蒙系统
行者966 小时前
Flutter到OpenHarmony:横竖屏自适应布局深度实践
flutter·harmonyos·鸿蒙