一、简介
并发是指在同一时间内,存在多个任务同时执行的情况。对于多核设备,这些任务可能同时在不同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;
}
}