并发概述
并发是指在同一时间内
,存在多个任务同时执行
的情况。对于多核设备
,这些任务可能同时在不同CPU
上并行执行
。对于单核设备
,多个并发任务不会在同一时刻并行执行
,但是CPU
会在某个任务休眠
或进行I/O操作
等状态下切换任务,调度执行其他任务,提升CPU的资源利用率。
- 异步并发
- 代码执行到一定程度会被
暂停
- 同一时间
只有
一段代码在执行
- 通过Promise和async/await提供异步并发能力
- 代码执行到一定程度会被
- 多线程并发
- 允许同一时间同时执行多段代码
- UI主线程响应用户操作更新同时,后台线程也可以执行耗时操作
- 通过TaskPool和Worker提供多线程并发能力
异步并发
Promise
Promise是一种用于处理异步操作的对象,可以将异步操作转换为类似于同步操作的方式,以方便代码编写和维护。
-
Promise 三种状态
-
pending(进行中)
刚创建时处于此状态
-
fulfilled(已完成)
操作成功后
-
rejected(已拒绝)
操作失败后
-
-
基本用法
-
创建
- 使用
new
关键字创建一个Promise - 使用
setTimeout
或其他异步 API 来模拟异步操作。 Promise
接受一个 executor 函数,包含resolve
和reject
回调- 成功时调用
resolve
,失败时调用reject
。 - then 处理 resolve,catch 处理 reject。
const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => { setTimeout(() => { const randomNumber: number = Math.random(); if (randomNumber > 0.5) { resolve(randomNumber); } else { reject(new Error('Random number is too small')); } }, 1000); })
- 使用
-
使用
- then可以接受两个参数then方法可接受两个参数,一个处理fulfilled状态的函数,另一个处理rejected状态的函数。此时上面的调用resolve后,返回第一个参数继续处理,上面的方法调用reject后返回第二个方法处理。
- 当只传一个参数时默认处理resolve后续事件,此时reject交由catch来处理。
- 当Promise被reject且未通过catch方法来处理时,会触发unhandledrejection事件,可使用errorManager.on('error')接口监听该事件,以全局捕获未处理的Promise reject。
promise.then((result)=>{ console.log("---->Promiss返回="+result); },(error: Error)=>{ console.log("---->Promiss返回1="+error); }).catch((error: Error)=>{ console.log("---->Promiss返回2="+error); });
-
-
async/await
async/await
是一种用于处理异步操作的Promise
语法糖,它们可以让编写异步代码变得更加简单和易读。通过使用async
关键字声明一个函数为异步函数,并使用await
关键字等待Promise
的解析(完成或拒绝),以同步的方式编写异步操作的代码
,避免回调带来的逻辑嵌套。 -
async
- 用于标识一个函数是异步函数
- 用async标识的函数内部可以使用await关键字等待一个Promise对象的异步操作,并返回其结果。
-
await
- 必须在async标识的函数内使用。
- 表示以同步的形式,等待一个异步函数返回结果。
-
示例
async myAsyncFunction(): Promise<string> {
const result: Promise<string> = new Promise((resolve: Function) => {
setTimeout(() => {
resolve('Hello, world!');
}, 3000);
});
return await result;
}
上面的示例中,使用await关键字来等待Promise的返回结果,并将结果存储在result中,返回给上一层。
多线程并发
概述
并发模型分为基于内存共享的并发模型和基于消息通信的并发模型。
-
内存共享模型
- 指多线程同时执行任务,
- 这些线程依赖同一内存并且都有权限访问,
- 线程访问内存前需要抢占并锁定内存的使用权,没有抢占到内存的线程需要等待其他线程释放使用权再执行。
图示:
代码示例:
- 共享内存容器
// 此段示例为伪代码仅作为逻辑示意,便于开发者理解使用内存共享模型和Actor模型的区别 class Queue { // ... push(value: number) {} empty(): boolean { // ... return true } pop(value: number) :number { // ... return value; } }
2. 内存锁
class Mutex { // ... lock(): boolean { // ... return true; } unlock() { } }
3. 操作管理类
class BufferQueue { queue: Queue = new Queue() mutex: Mutex = new Mutex() add(value: number) { // 尝试获取锁 if (this.mutex.lock()) { this.queue.push(value) this.mutex.unlock() } } take(value: number): number { let res: number = 0; // 尝试获取锁 if (this.mutex.lock()) { if (this.queue.empty()) { res = 1; } let num: number = this.queue.pop(value) this.mutex.unlock() res = num; } return res; } }
4. 全局共享的内存
// 构造一段全局共享的内存 let g_bufferQueue = new BufferQueue() class Producer { constructor() { } run() { let value = Math.random() // 跨线程访问bufferQueue对象 g_bufferQueue.add(value) } }
5. 消费者
class ConsumerTest { constructor() { } run() { // 跨线程访问bufferQueue对象 let num = 123; let res = g_bufferQueue.take(num) if (res != null) { // 添加消费逻辑 } } }
6. 生产者和使用示例
function Main(): void { let consumer: ConsumerTest = new ConsumerTest() let producer1: Producer = new Producer() for (let i = 0;i < 0;i++) { // 模拟启动多线程执行生产任务 // let thread = new Thread() // thread.run(producer.run()) // consumer.run() } }
-
消息通信模型
- Actor
- 基于消息通信的并发模型,不同角色之间并不共享内存没有锁困扰。
- ArkTS提供了TaskPool和Worker两种并发能力
- TaskPool和Worker都基于Actor并发模型实现
- 生产者线程和UI线程都有自己的虚拟机实例,两个虚拟机实例之间拥有独占的内存,相互隔离。
- 生产者生产出结果后通过序列化通信将结果发送给UI线程,
- UI线程消费结果后再发送新的生产任务给生产者线程。
图示:
代码示例:
- 生产者异步方法
import { taskpool } from '@kit.ArkTS'; // 跨线程并发任务 @Concurrent async function produce(): Promise<number> { // 添加生产相关逻辑 console.info("producing..."); return Math.random(); }
2. 消费者类
class Consumer { public consume(value: Object) { // 添加消费相关逻辑 console.info("consuming value: " + value); } }
3. 使用实例
@Entry @Component struct Index { @State message: string = 'Hello World' build() { Row() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) Button() { Text("start") }.onClick(() => { //使用生产者异步方法创建任务 let produceTask: taskpool.Task = new taskpool.Task(produce); //使用消费之类创建消费者 let consumer: Consumer = new Consumer(); //使用for循环生成十个生产及消费任务 for (let index: number = 0; index < 10; index++) { // 执行生产异步并发任务 taskpool.execute(produceTask).then((res: Object) => { consumer.consume(res); }).catch((e: Error) => { console.error(e.message); }) } }) .width('20%') .height('20%') } .width('100%') } .height('100%') } }
- Actor
TaskPool
任务池(TaskPool)作用是为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性能,且您无需关心线程实例的生命周期。具体接口信息及使用方法详情请见TaskPool。
-
运作机制
-
宿主线程封装任务抛给任务队列,系统选择合适的工作线程,进行任务的分发及执行,再将结果返回给宿主线程
-
支持任务的执行、取消,以及指定优先级的能力
-
通过系统统一线程管理,结合动态调度及负载均衡算法,可以节约系统资源。
-
系统默认会启动一个任务工作线程,当任务较多时会扩容,工作线程数量上限跟当前设备的物理核数相关,具体数量内部管理,保证最优的调度及执行效率,长时间没有任务分发时会缩容,减少工作线程数量。
运作机制示意图:
-
-
TaskPool注意事项
- 实现任务的函数需要使用@Concurrent装饰器标注,且仅支持在.ets文件中使用。
- 从API version 11开始,跨并发实例传递带方法的实例对象时,该类必须使用装饰器@Sendable装饰器标注,且仅支持在.ets文件中使用。
- 任务函数在TaskPool工作线程的执行耗时不能超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),否则会被强制退出。
- 实现任务的函数入参需满足序列化支持的类型,详情请参见线程间通信对象。
- ArrayBuffer参数在TaskPool中默认转移,需要设置转移列表的话可通过接口setTransferList()设置。
- 由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。
- 序列化传输的数据量大小限制为16MB。
- Priority的IDLE优先级是用来标记需要在后台运行的耗时任务(例如数据同步、备份),它的优先级别是最低的。这种优先级标记的任务只会在所有线程都空闲的情况下触发执行,并且只会占用一个线程来执行。
- Promise不支持跨线程传递,如果TaskPool返回pending或rejected状态的Promise,会返回失败;对于fulfilled状态的Promise,TaskPool会解析返回的结果,如果结果可以跨线程传递,则返回成功。
- 不支持在TaskPool工作线程中使用AppStorage。
-
@Concurrent装饰器
在使用TaskPool时,执行的并发函数需要使用该装饰器修饰,否则无法通过相关校验。更多示例及说明请点我点我进入详细说明页。
Worker
Worker主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与宿主线程分离,在后台线程中运行一个脚本进行耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞宿主线程的运行。具体接口信息及使用方法详情请见Worker。
- 运作机制
-
创建Worker的线程称为宿主线程(不一定是主线程,工作线程也支持创建Worker子线程)
-
Worker自身的线程称为Worker子线程(或Actor线程、工作线程)
-
每个Worker子线程与宿主线程拥有独立的实例,包含基础设施、对象、代码段等,因此每个Worker启动存在一定的内存开销,需要限制Worker的子线程数量
-
Worker子线程和宿主线程之间的通信是基于消息传递的,Worker通过序列化机制与宿主线程之间相互通信,完成命令及数据交互。
运作机制示意图
-
-
使用方法
- 创建Worker文件及配置
- 手动创建
-
在{moduleName}/src/main/ets/workers/目录下创建work文件 import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
const workerPort: ThreadWorkerGlobalScope = worker.workerPort; /** * Defines the event handler to be called when the worker thread receives a message sent by the host thread. * The event handler is executed in the worker thread. * * @param e message data */ workerPort.onmessage = (e: MessageEvents) => { workerPort.postMessage() } /** * Defines the event handler to be called when the worker receives a message that cannot be deserialized. * The event handler is executed in the worker thread. * * @param e message data */ workerPort.onmessageerror = (e: MessageEvents) => { } /** * Defines the event handler to be called when an exception occurs during worker execution. * The event handler is executed in the worker thread. * * @param e error message */ workerPort.onerror = (e: ErrorEvent) => { }
-
配置build-profile.json5 "buildOption": { "sourceOption": { "workers": [ "./src/main/ets/workers/worker.ets" ] } }
-
- 自动创建
-
DevEco Studio支持一键生成Worker
在对应的{moduleName}目录下任意位置,点击鼠标右键 > New > Worker,即可自动生成Worker的模板文件及配置信息,无需再手动在build-profile.json5中进行相关配置。
-
- 手动创建
-
加载Worker及通信
-
导入模块:
import { worker } from '@kit.ArkTS';
-
创建Worker:
const workerInstance: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/Worker.ets');
-
发送消息到Worker:
workerInstance.postMessage({ type: 'start' });
- 在Worker中接收消息:
workerPort.onmessage = (e: MessageEvents) => { const type = e.data.type as string; if (type === 'start') { performTask(); } };
-
关闭Worker:
当不再需要Worker时,应当将其关闭以释放资源。
workerInstance.terminate();
-
- 创建Worker文件及配置
-
Worker注意事项
- 创建Worker时,有手动和自动两种创建方式,手动创建Worker线程目录及文件时,还需同步进行相关配置,详情请参考创建Worker的注意事项。
- 使用Worker能力时,构造函数中传入的Worker线程文件的路径在不同版本有不同的规则,详情请参见文件路径注意事项。
- Worker创建后需要手动管理生命周期,且最多同时运行的Worker子线程数量为64个,详情请参见生命周期注意事项。
- 由于不同线程中上下文对象是不同的,因此Worker线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。
- 序列化传输的数据量大小限制为16MB。
- 使用Worker模块时,需要在宿主线程中注册onerror接口,否则当Worker线程出现异常时会发生jscrash问题。
- 不支持跨HAP使用Worker线程文件。
- 引用HAR/HSP前,需要先配置对HAR/HSP的依赖,详见引用共享包。
- 不支持在Worker工作线程中使用AppStorage。
TaskPool和Worker的对比
- 实现特点对比
表1 TaskPool和Worker的实现特点对比
实现 | TaskPool | Worker |
---|---|---|
内存模型 | 线程间隔离,内存不共享。 | 线程间隔离,内存不共享。 |
参数传递机制 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 |
参数传递 | 直接传递,无需封装,默认进行transfer。 | 消息对象唯一参数,需要自己封装。 |
方法调用 | 直接将方法传入调用。 | 在Worker线程中进行消息解析并调用对应方法。 |
返回值 | 异步调用后默认返回。 | 主动发送消息,需在onmessage解析赋值。 |
生命周期 | TaskPool自行管理生命周期,无需关心任务负载高低。 | 开发者自行管理Worker的数量及生命周期。 |
任务池个数上限 | 自动管理,无需配置。 | 同个进程下,最多支持同时开启64个Worker线程,实际数量由进程内存决定。 |
任务执行时长上限 | 3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),长时任务无执行时长上限。 | 无限制。 |
设置任务的优先级 | 支持配置任务优先级。 | 不支持。 |
执行任务的取消 | 支持取消已经发起的任务。 | 不支持。 |
线程复用 | 支持。 | 不支持。 |
任务延时执行 | 支持。 | 不支持。 |
设置任务依赖关系 | 支持。 | 不支持。 |
串行队列 | 支持。 | 不支持。 |
任务组 | 支持。 | 不支持。 |
- 适用场景对比
TaskPool和Worker均支持多线程并发能力。由于TaskPool的工作线程会绑定系统的调度优先级,并且支持负载均衡(自动扩缩容),而Worker需要开发者自行创建,存在创建耗时以及不支持设置调度优先级,故在性能方面使用TaskPool会优于Worker,因此大多数场景推荐使用TaskPool。
TaskPool偏向独立任务维度,该任务在线程中执行,无需关注线程的生命周期,超长任务(大于3分钟且非长时任务)会被系统自动回收;而Worker偏向线程的维度,支持长时间占据线程执行,需要主动管理线程生命周期。
常见的一些开发场景及适用具体说明如下:
- 运行时间超过3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时)的任务。例如后台进行1小时的预测算法训练等CPU密集型任务,需要使用Worker。场景示例可参考常驻任务开发指导。
- 有关联的一系列同步任务。例如在一些需要创建、使用句柄的场景中,句柄创建每次都是不同的,该句柄需永久保存,保证使用该句柄进行操作,需要使用Worker。场景示例可参考使用Worker处理关联的同步任务。
- 需要设置优先级的任务。例如图库直方图绘制场景,后台计算的直方图数据会用于前台界面的显示,影响用户体验,需要高优先级处理,需要使用TaskPool。
- 需要频繁取消的任务。例如图库大图浏览场景,为提升体验,会同时缓存当前图片左右侧各2张图片,往一侧滑动跳到下一张图片时,要取消另一侧的一个缓存任务,需要使用TaskPool。
- 大量或者调度点较分散的任务。例如大型应用的多个模块包含多个耗时任务,不方便使用Worker去做负载管理,推荐采用TaskPool。场景示例可参考批量数据写数据库场景。
并发线程间通信
线程间通信指的是并发多线程间存在的数据交换行为。
线程间通信对象
-
普通对象
- 普通对象跨线程时通过拷贝形式传递,两个线程的对象内容一致,但是指向各自线程的隔离内存区间,被分配在各自线程的虚拟机本地堆(LocalHeap)
- 例如Ecmascript262规范定义的Object、Array、Map等对象是通过这种方式实现跨并发实例通信的。
示例图:
-
定义普通对象 // Test.ets // 自定义class TestA export class TestA { constructor(name: string) { this.name = name; } name: string = 'ClassA'; }
-
使用 // Index.ets 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%') } }
-
ArrayBuffer对象
请看官方文档
-
SharedArrayBuffer对象
请看官方文档
-
Transferable对象
请看官方文档
-
Sendable对象
请看官方文档