以下内容均基于HarmonyOS NEXT版本
一、引言
在现代应用开发中,并发编程是提升应用性能和响应速度的关键技术。ArkTS作为HarmonyOS应用开发的重要编程语言,提供了强大的并发能力,包括异步并发和多线程并发。
二、并发概述
2.1 并发的概念
并发是指在同一时间段内,能够处理多个任务的能力。在计算机科学中,并发可以提升系统的吞吐量、响应速度和资源利用率,并能更好地处理多用户、多线程和分布式的场景。
2.2 ArkTS的并发策略
为了提升应用的响应速度与帧率,避免耗时任务对UI主线程的影响,ArkTS提供了异步并发和多线程并发两种处理策略:
- 异步并发:异步代码在执行到一定程度后会被暂停,以便在未来某个时间点继续执行,这种情况下,同一时间只有一段代码在执行。
- 多线程并发:允许在同一时间段内同时执行多段代码。在主线程继续响应用户操作和更新UI的同时,后台也能执行耗时操作,从而避免应用出现卡顿。
三、异步并发
3.1 Promise
3.1.1 概念
Promise是一种用于处理异步操作的对象,它表示一个可能还未完成的操作,并提供了一系列方法来处理操作的结果或错误。Promise对象有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已失败)。当操作完成时,Promise对象将会从pending状态转变为fulfilled或rejected状态,并调用相应的回调函数。
3.1.2 基本用法
typescript
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const randomNumber = Math.random();
if (randomNumber > 0.5) {
resolve(randomNumber);
} else {
reject(new Error('Random number is too small'));
}
}, 1000);
});
promise
.then((value) => {
console.log('操作成功:', value);
})
.catch((error) => {
console.error('操作失败:', error);
})
.finally(() => {
console.log('操作完成');
});
3.1.3 适用场景
Promise适用于处理简单的异步操作,例如一次网络请求、一次文件读写等操作。
3.2 async/await
3.2.1 概念
async/await是一种用于处理异步操作的语法糖,它基于Promise对象提供了一种更直观、更方便的方式来编写和处理异步代码。
3.2.2 基本用法
javascript
async function myAsyncFunction() {
try {
const result = await new Promise((resolve) => {
setTimeout(() => {
resolve('Hello, world!');
}, 3000);
});
console.info(result); // 输出: Hello, world!
} catch (e) {
console.error(`Get exception: ${e}`);
}
}
myAsyncFunction();
3.2.3 优点
- 代码可读性更高,更接近同步代码的写法,易于理解和维护。
- 可以在代码中使用try/catch语句来捕获和处理异步操作产生的错误。
- 可以使用常规的控制流语法(如循环、条件语句)来组织和管理异步代码的执行顺序。
3.2.4 适用场景
async/await适用于处理复杂的异步流程,尤其适用于多层依赖的异步操作。
3.3 异步并发的应用场景
异步并发适用于单次I/O任务的场景开发,例如一次网络请求、一次文件读写等操作。以下是一个使用async/await进行异步文件写入的示例:
typescript
import fs from '@ohos.file.fs';
async function writeFileAsync(filePath: string, content: string): Promise<void> {
try {
const file = await fs.open(filePath, fs.OpenMode.WRITE | fs.OpenMode.CREATE);
await fs.writeFile(file.fd, content);
console.log('File written successfully');
await fs.close(file);
} catch (error) {
console.error(`Error occurred: ${error}`);
}
}
// 使用示例
const filePath = '/data/test.txt';
const content = 'Hello, HarmonyOS!';
writeFileAsync(filePath, content);
四、多线程并发
4.1 并发模型
并发模型是用来实现不同应用场景中并发任务的编程模型,常见的并发模型分为基于内存共享的并发模型和基于消息通信的并发模型。Actor并发模型作为基于消息通信并发模型的典型代表,不需要开发者去面对锁带来的一系列复杂偶发的问题,同时并发度也相对较高,因此得到了广泛的支持和使用。当前ArkTS提供的TaskPool和Worker两种并发能力,都基于Actor并发模型实现。
4.2 TaskPool
4.2.1 作用
任务池(TaskPool)作用是为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性能,且开发者无需关心线程实例的生命周期。
4.2.2 运作机制
TaskPool支持开发者在主线程封装任务抛给任务队列,系统选择合适的工作线程,进行任务的分发及执行,再将结果返回给主线程。系统默认会启动一个任务工作线程,当任务较多时会扩容,工作线程数量上限跟当前设备的物理核数相关,具体数量内部管理,保证最优的调度及执行效率,长时间没有任务分发时会缩容,减少工作线程数量。
4.2.3 使用示例
以下是一个使用TaskPool进行图片加载的示例:
typescript
// IconItemSource.ets
export class IconItemSource {
image: string | Resource = '';
text: string | Resource = '';
constructor(image: string | Resource = '', text: string | Resource = '') {
this.image = image;
this.text = text;
}
}
// IndependentTask.ets
import { IconItemSource } from './IconItemSource';
// 在TaskPool线程中执行的方法,需要添加@Concurrent注解,否则无法正常调用。
@Concurrent
export function loadPicture(count: number): IconItemSource[] {
let iconItemSourceList: IconItemSource[] = [];
// 遍历添加6*count个IconItem的数据
for (let index = 0; index < count; index++) {
const numStart: number = index * 6;
// 此处循环使用6张图片资源
iconItemSourceList.push(new IconItemSource('$media:startIcon', `item${numStart + 1}`));
iconItemSourceList.push(new IconItemSource('$media:background', `item${numStart + 2}`));
iconItemSourceList.push(new IconItemSource('$media:foreground', `item${numStart + 3}`));
iconItemSourceList.push(new IconItemSource('$media:startIcon', `item${numStart + 4}`));
iconItemSourceList.push(new IconItemSource('$media:background', `item${numStart + 5}`));
iconItemSourceList.push(new IconItemSource('$media:foreground', `item${numStart + 6}`));
}
return iconItemSourceList;
}
// MultiTask.ets
import { taskpool } from '@kit.ArkTS';
import { IconItemSource } from './IconItemSource';
import { loadPicture } from './IndependentTask';
let iconItemSourceList: IconItemSource[][] = [];
let taskGroup: taskpool.TaskGroup = new taskpool.TaskGroup();
taskGroup.addTask(new taskpool.Task(loadPicture, 30));
taskGroup.addTask(new taskpool.Task(loadPicture, 20));
taskGroup.addTask(new taskpool.Task(loadPicture, 10));
taskpool.execute(taskGroup).then((ret: object) => {
let tmpLength = (ret as IconItemSource[][]).length;
for (let i = 0; i < tmpLength; i++) {
for (let j = 0; j < ret[i].length; j++) {
iconItemSourceList.push(ret[i][j]);
}
}
});
4.2.4 注意事项
- 实现任务的函数需要使用装饰器@Concurrent标注,且仅支持在.ets文件中使用。
- 从API version 11开始,实现任务的函数需要使用类方法时,该类必须使用装饰器@Sendable标注,且仅支持在.ets文件中使用。
- 任务函数在TaskPool工作线程的执行耗时不能超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),否则会被强制退出。
- 实现任务的函数入参需满足序列化支持的类型。
- ArrayBuffer参数在TaskPool中默认转移,需要设置转移列表的话可通过接口setTransferList()设置。
- 由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。
- 序列化传输的数据量大小限制为16MB。
- Priority的IDLE优先级是用来标记需要在后台运行的耗时任务(例如数据同步、备份),它的优先级别是最低的。这种优先级标记的任务只会在所有线程都空闲的情况下触发执行,并且只会占用一个线程来执行。
- Promise不支持跨线程传递。
4.3 Worker
4.3.1 作用
Worker主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与主线程分离,在后台线程中运行一个脚本进行耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞主线程的运行。
4.3.2 运作机制
创建Worker的线程称为宿主线程(不一定是主线程,工作线程也支持创建Worker子线程),Worker自身的线程称为Worker子线程(或Actor线程、工作线程)。每个Worker子线程与宿主线程拥有独立的实例,包含基础设施、对象、代码段等,因此每个Worker启动存在一定的内存开销,需要限制Worker的子线程数量。Worker子线程和宿主线程之间的通信是基于消息传递的,Worker通过序列化机制与宿主线程之间相互通信,完成命令及数据交互。
4.3.3 使用示例
typescript
// Index.ets
import { worker } from '@kit.ArkTS';
@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. 创建Worker实例
const myWorker = new worker.ThreadWorker('entry/ets/workers/Worker.ets');
// 2. 接收Worker返回的结果
myWorker.onmessage = (e) => {
console.log('主线程收到最终结果:', e.data.result);
myWorker.terminate(); // 销毁Worker
};
// 3. 向Worker发送启动指令
myWorker.postMessage({ type: 'start', data: 10 });
})
}
.height('100%')
.width('100%')
}
}
// Worker.ets
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
import { taskpool } from '@kit.ArkTS';
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
workerPort.onmessage = async (e: MessageEvents) => {
if (e.data.type === 'start') {
// 模拟Worker数据处理
const processedData = heavyComputation(e.data.data);
// 调用TaskPool执行并发任务
const task = new taskpool.Task(parallelTask, processedData);
const result = await taskpool.execute(task);
console.log('Worker线程返回结果: ', result);
// 将最终结果返回主线程
workerPort.postMessage({
status: 'success',
result: result
});
}
}
function heavyComputation(base: number): number {
let sum = 0;
for (let i = 0; i < base * 10; i++) {
sum += Math.sqrt(i);
}
return sum;
}
@Concurrent
function parallelTask(base: number): number {
let total = 0;
for (let i = 0; i < base; i++) {
total += i % 2 === 0 ? i : -i;
}
console.log('TaskPool线程计算结果: ', total);
return total;
}
4.3.4 注意事项
- 创建Worker时,有手动和自动两种创建方式,手动创建Worker线程目录及文件时,还需同步进行相关配置。
- 使用Worker能力时,构造函数中传入的Worker线程文件的路径在不同版本有不同的规则。
- Worker创建后需要手动管理生命周期,且最多同时运行的Worker子线程数量为64个。
- 由于不同线程中上下文对象是不同的,因此Worker线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。
- 序列化传输的数据量大小限制为16MB。
- 使用Worker模块时,需要在宿主线程中注册onerror接口,否则当Worker线程出现异常时会发生jscrash问题。
- 不支持跨HAP使用Worker线程文件。
- 引用HAR/HSP前,需要先配置对HAR/HSP的依赖。
- 不支持在Worker工作线程中使用[AppStorage]。
4.4 TaskPool和Worker的对比
对比项 | TaskPool | Worker |
---|---|---|
实现特点 | 可以直接传递参数、直接接收返回数据,任务池个数上限自动管理,支持设置任务优先级和取消任务,任务执行时长同步代码上限3分钟,异步代码无限 | 需要自行封装参数,通过onmessage接收返回数据,最多64个线程,具体看内存,不支持设置优先级和取消任务,任务执行时长无限 |
适用场景 | 适合需要设置优先级的任务、需要频繁取消的任务、大量或者调度点较分散的任务 | 适合运行时间超过3分钟的任务、有关联的一系列同步任务 |
五、并发线程间通信
在并发多线程场景下,不同并发线程间需要进行数据通信,不同类别对象的传输方式存在差异,包括拷贝或内存共享等。
5.1 普通对象
普通对象跨线程时通过拷贝形式传递。
5.2 ArrayBuffer对象
ArrayBuffer内部包含一块Native内存。其JS对象壳与普通对象一样,需要经过序列化与反序列化拷贝传递,但是Native内存有两种传输方式:拷贝和转移。
5.3 SharedArrayBuffer对象
SharedArrayBuffer内部包含一块Native内存,支持跨并发实例间共享,但是访问及修改需要采用Atomics类,防止数据竞争。
5.4 Transferable对象
Transferable对象(也称为NativeBinding对象)指的是一个JS对象,绑定了一个C++对象,且主体功能由C++提供,可以实现共享和转移模式。
5.5 Sendable对象
ArkTS提供了Sendable对象类型,在并发通信时支持通过引用传递。Sendable对象为可共享的,其跨线程前后指向同一个JS对象,如果其包含了JS或者Native内容,均可以直接共享,如果底层是Native实现的,则需要考虑线程安全性。
六、总结
ArkTS的并发能力为开发者提供了强大的工具,通过异步并发和多线程并发,可以有效地提升应用的响应速度和性能。异步并发适用于单次I/O任务,而多线程并发适用于CPU密集型任务、I/O密集型任务和同步任务等。TaskPool和Worker是多线程并发的两种实现方式,开发者可以根据具体的业务场景选择合适的方式。同时,在并发编程中,需要注意线程间通信和数据传输的问题,确保程序的正确性和稳定性。希望本文能够帮助初学者快速掌握ArkTS并发的相关知识,在实际开发中灵活运用。