鸿蒙ArkTS的并发实现原理与应用

前端 潘老师 2周前 (04-07) 17 ℃ (0) 扫码查看

鸿蒙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核心要点

  1. 三种状态:Promise有三种状态,分别是pending(进行中)、fulfilled(成功)和rejected(失败) 。状态一旦从pending变为fulfilled或者rejected,就不能再改变了。
  2. 基本用法
const promise = new Promise((resolve, reject) => { 
  // 异步操作(如setTimeout、文件读写)
  if (成功) resolve(result); 
  else reject(error); 
});
  1. 链式调用
promise.then(result => { ... })
  .catch(error => { ... });
  1. 关键注意点:如果没有处理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核心要点

  1. 语法规则
    • 用async标记异步函数,比如async function fetchData()
    • 使用await暂停函数执行,直到Promise完成,例如await promise
  2. 代码示例
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捕获
  }
}
  1. 优势与限制
    • 优势:使用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)

  1. 作用:为应用提供多线程运行环境,降低资源消耗,提升系统整体性能。开发者无需操心线程实例的生命周期。
  2. 运作机制:可以参考相关官方文档中的图示来深入理解其运作原理。
  3. 核心要点
    • 使用场景:适用于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

  1. 作用:为应用提供多线程运行环境,能让应用在后台线程执行耗时操作,与宿主线程分离,避免阻塞宿主线程,比如计算密集型或高延迟任务。
  2. 运作机制:具体运作机制可查看官方文档中的相关图示。
  3. 核心要点
    • 使用场景:适合长时任务,如后台下载、持续数据同步;以及有明确控制线程生命周期需求的场景。
    • 关键规则
      • 最多同时运行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的具体实现特点,可以查阅官方文档获取更详细的信息。通过合理运用这两种并发策略,我们能够更好地优化鸿蒙应用的性能。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/front/16852.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】