章
目
录
鸿蒙ArkTS的并发机制能让应用在处理多个任务时更加高效,避免卡顿,提升用户体验。下面,我们就来详细了解一下ArkTS是如何实现并发的。
一、并发的基本概念
并发指的是在同一时间段内,有多个任务同时在执行。这里要注意区分并发和并行:
- 并发:从逻辑上来说,多个任务像是在同时执行。在单核设备上,CPU会在任务处于休眠或者进行I/O操作等状态时,切换去执行其他任务,以此来提高CPU的资源利用率;而在多核设备上,这些任务是可以真正并行执行的。
- 并行:这是指物理上的同时执行,不过这需要依赖多核设备才能实现。
并发的主要目标就是提升应用的响应速度和帧率,避免主线程被阻塞,从而让用户操作更加流畅。
二、ArkTS的并发策略
为了提升应用性能,避免耗时任务影响UI主线程,ArkTS提供了两种并发策略:异步并发和多线程并发。
(一)异步并发
异步代码在执行过程中,会在某个阶段暂停,之后在合适的时间点继续执行。在同一时刻,实际上只有一段代码在运行。ArkTS借助Promise和async/await实现异步并发,这种方式适用于单次I/O任务的开发场景,比如网络请求或者文件读写操作。
(二)多线程并发
多线程并发允许在同一时间段内,有多段代码同时执行。这样,在UI主线程继续响应用户操作、更新UI的同时,后台线程可以去执行那些比较耗时的操作,防止应用出现卡顿现象。ArkTS通过TaskPool(线程池)和Worker(独立线程)来提供多线程并发能力,很适合处理CPU密集型任务以及其他耗时操作,如图像处理等场景。
三、ArkTS并发策略对比
下面通过表格来详细对比这两种并发策略:
策略 | 异步并发 | 多线程并发 |
---|---|---|
实现方式 | Promise + async/await | TaskPool(线程池) + Worker(独立线程) |
执行特性 | 单线程,代码分段暂停和恢复 | 多线程并行执行 |
适用场景 | 单次I/O任务(如网络请求、文件读写) | CPU密集型任务、耗时操作(如图像处理) |
线程阻塞 | 不会阻塞主线程,但代码是串行执行 | 后台线程执行,能完全避免主线程卡顿 |
资源开销 | 较低(因为没有线程创建的开销) | 较高(需要管理线程的生命周期) |
(一)异步并发要点总结
- 本质:异步并发本质上是单线程任务调度,通过事件循环来实现非阻塞操作。
- 优势:这种方式比较轻量级,对于简单的I/O任务处理起来很合适。
- 限制:它无法充分利用多核性能,如果处理复杂任务,可能会出现回调嵌套的情况,不过可以使用async/await来优化。
(二)多线程并发要点总结
- 核心对象:
- TaskPool:主要作用是复用线程,避免频繁创建和销毁线程,从而降低资源消耗。
- Worker:作为独立线程,适合执行长时间运行的任务。
- 数据传输:
- 基本类型:基本类型的数据可以直接进行拷贝传输。
- 复杂对象:复杂对象在传输时,需要进行序列化(比如使用JSON),或者使用共享内存(不过要谨慎使用)。要注意,线程间通信得通过消息传递的方式,避免共享资源竞争。
四、应用场景选择
不同的任务类型适合不同的并发策略,具体如下:
任务类型 | 推荐策略 | 示例场景 |
---|---|---|
单次I/O操作 | 异步并发 | 请求API、读取本地文件 |
CPU密集型 | 多线程(TaskPool) | 图像处理、数据加密解密 |
长时任务 | 多线程(Worker) | 后台下载、持续日志写入 |
UI交互响应 | 主线程 + 异步 | 按钮点击后非阻塞更新UI |
五、异步并发详解(Promise和async/await)
(一)异步并发核心概念
异步并发本质上是单线程非阻塞任务调度,同一时间只有一段代码在执行。它依靠事件循环来实现任务的挂起和恢复,这样就能避免主线程被阻塞。不过,它不太适合处理CPU密集型任务,因为这类任务会阻塞主线程。
(二)Promise与async/await对比
特性 | Promise | async/await |
---|---|---|
本质 | 异步状态管理对象 | Promise的语法糖,让异步代码编写更简单 |
代码风格 | 通过链式调用(.then().catch())来处理 | 采用同步式写法,逻辑更直观 |
错误处理 | 使用.catch()捕获异常 | 利用try/catch捕获异常 |
返回值 | 返回Promise对象 | 返回Promise对象 |
可读性 | 当回调嵌套复杂时,可读性较差 | 代码线性执行,可读性高 |
(三)Promise核心要点
- 三种状态:Promise有三种状态,分别是pending(进行中)、fulfilled(成功)和rejected(失败) 。状态一旦从pending变为fulfilled或者rejected,就不能再改变了。
- 基本用法:
const promise = new Promise((resolve, reject) => {
// 异步操作(如setTimeout、文件读写)
if (成功) resolve(result);
else reject(error);
});
- 链式调用:
promise.then(result => { ... })
.catch(error => { ... });
- 关键注意点:如果没有处理reject状态,会触发unhandledrejection事件,所以需要进行全局监听:
errorManager.on('error', (err) => { ... });
下面是一个完整示例,创建了一个Promise对象并模拟异步操作:
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);})
上述代码使用setTimeout模拟异步操作,1秒钟后随机生成一个数字。如果数字大于0.5,就执行resolve回调函数并传递随机数;否则执行reject回调函数并传递错误对象。
(四)async/await核心要点
- 语法规则:
- 用async标记异步函数,比如
async function fetchData()
。 - 使用await暂停函数执行,直到Promise完成,例如
await promise
。
- 用async标记异步函数,比如
- 代码示例:
async function myAsyncFunc(): Promise<string> {
try {
const result = await new Promise(resolve => {
setTimeout(() => resolve('Hello'), 3000);
});
return result;
} catch (error) {
console.error(error);
throw error; // 抛出异常会被外层catch捕获
}
}
- 优势与限制:
- 优势:使用async/await可以让代码扁平化,有效避免回调地狱。
- 限制:如果在循环中滥用await,可能会导致性能下降。
下面是一个完整示例,模拟以同步方式执行异步操作,3秒钟后返回字符串:
async function myAsyncFunction(): Promise<string> {
const result: string = await new Promise((resolve: Function) => {
setTimeout(() => {
resolve('Hello, world!'); }, 3000); });
console.info(result); // 输出: Hello, world! return result;}
@Entry@Componentstruct Index {
@State message: string = 'Hello World';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(async () => {
let res = await myAsyncFunction();
console.info("res is: " + res);
})}
.width('100%')
}
.height('100%')
}}
(五)错误处理对比
方式 | Promise | async/await |
---|---|---|
成功处理 | .then(result => { … }) | const result = await promise |
失败处理 | .catch(error => { … }) | try { … } catch (error) { … } |
全局异常捕获 | 监听unhandledrejection事件 | 结合try/catch与全局监听 |
六、多线程并发(TaskPool和Worker)
(一)任务池(TaskPool)
- 作用:为应用提供多线程运行环境,降低资源消耗,提升系统整体性能。开发者无需操心线程实例的生命周期。
- 运作机制:可以参考相关官方文档中的图示来深入理解其运作原理。
- 核心要点:
- 使用场景:适用于CPU密集型的短任务,如图像处理、加密解密;以及高频小任务,能避免频繁创建线程带来的开销。
- 关键规则:
- 任务函数必须用
@Concurrent
修饰。 - 单任务函数执行时间(不含异步I/O)应小于等于3分钟。
- 数据传输仅支持可序列化类型,像基本类型、ArrayBuffer等。
- 任务函数必须用
- 示例代码:
// 定义任务函数(必须@Concurrent)
@Concurrent
function add(a: number, b: number): number {
return a + b;
}
// 提交任务到线程池
async function runTask() {
const task = new taskpool.Task(add, 1, 2);
const result = await taskpool.execute(task);
console.log(`Task结果: ${result}`); // 输出3
}
- **注意事项**:
- 任务函数内禁止访问外部变量,避免闭包。
- 只能使用线程安全的API,不能直接操作UI。
(二)Worker
- 作用:为应用提供多线程运行环境,能让应用在后台线程执行耗时操作,与宿主线程分离,避免阻塞宿主线程,比如计算密集型或高延迟任务。
- 运作机制:具体运作机制可查看官方文档中的相关图示。
- 核心要点:
- 使用场景:适合长时任务,如后台下载、持续数据同步;以及有明确控制线程生命周期需求的场景。
- 关键规则:
- 最多同时运行64个Worker。
- 必须手动调用
close()
销毁Worker。 - Worker文件需要放在指定目录,例如
entry/ets/workers/
。
- 示例代码:
// 主线程:创建Worker并通信
const worker = new worker.ThreadWorker('entry/ets/workers/myWorker.ets');
worker.postMessage('开始任务'); // 发送消息
worker.onmessage = (e) => {
console.log(`收到Worker回复: ${e.data}`);
};
// Worker线程(myWorker.ets)
workerPort.onmessage = (e) => {
workerPort.postMessage('任务完成'); // 回复消息
};
- **注意事项**:
- 如果存在多级Worker,父Worker销毁前要先销毁子Worker。
- 所有Worker的内存总和不能超过1.5GB或者物理内存的60%。
(三)TaskPool vs Worker核心对比
特性 | TaskPool | Worker |
---|---|---|
本质 | 线程池动态调度(任务队列 + 线程复用) | 独立线程(需手动管理生命周期) |
适用场景 | 短时、高频的CPU密集型任务(如计算) | 长时、独立的后台任务(如下载、日志) |
线程数量 | 动态扩容(上限为设备物理核数) | 最多64个,需手动销毁 |
开发复杂度 | 低(系统自动管理线程) | 较高(需处理线程创建、销毁、通信) |
数据传输限制 | 16MB(支持序列化对象) | 16MB(支持序列化对象) |
生命周期 | 任务结束自动释放线程资源 | 需手动调用close()或terminate()销毁 |
错误处理 | 通过Promise的.catch()捕获 | 通过onerror回调捕获 |
(四)场景选择口诀
可以用这样一句口诀来帮助选择合适的并发方式:“TaskPool扛短快,Worker长时独立在;高频计算用池化,下载日志Worker带。”
(五)错误处理对比
策略 | 错误捕获方式 | 示例 |
---|---|---|
TaskPool | 通过Promise的.catch() | taskpool.execute(task).catch(e => {}) |
Worker | 通过onerror回调 | worker.onerror = (err) => { … } |
(六)记忆要点
- TaskPool:主要特点是线程池复用,适用于短任务,使用
@Concurrent
装饰器,并且参数需要是可序列化的。 - Worker:作为独立线程,需要手动管理生命周期,主从线程之间通过
postMessage
进行通信。
如果想要深入了解TaskPool和Worker的具体实现特点,可以查阅官方文档获取更详细的信息。通过合理运用这两种并发策略,我们能够更好地优化鸿蒙应用的性能。