ArkTS并发官方指南,包括并发概述异步并发 (Promise和async/await)多线程并发并发线程间通信应用多线程开发实践并发常见问题

并发概述

为了提升应用的响应速度和帧率,避免耗时任务影响UI主线程,ArkTS提供了异步和多线程两种处理策略。

  • 异步是指异步代码在执行到一定程度后暂停,并在未来某个时间点继续执行,同一时间只有一段代码执行。ArkTS通过Promiseasync/await提供异步并发能力,适用于单次I/O任务。详细请参见使用异步并发能力

  • 多线程允许同时执行多段代码。UI主线程继续响应用户操作和更新UI,后台线程执行耗时操作,避免应用卡顿。ArkTS通过TaskPoolWorker提供多线程能力,适用于耗时任务等场景。详细请参见多线程并发概述

在多线程场景下,不同线程间需要进行数据通信。不同类别的对象采用不同的传输方式,如拷贝或内存共享。

并发能力广泛应用于多种场景,包括异步并发任务耗时任务(如CPU密集型任务I/O密集型任务同步任务等)、长时任务常驻任务等。开发者可以根据不同的任务诉求和场景,选择相应的并发策略进行优化和开发,具体案例可以参见应用多线程开发实践案例

异步 (Promise和async/await)

Promise

概述

Promise是一种用于处理异步操作的对象,可将异步操作转换为类似同步操作的风格,便于代码编写和维护。Promise通过状态机制管理异步操作的不同阶段,有三种状态:pending(进行中)、fulfilled(已完成,也叫resolved)和rejected(已拒绝)。创建后处于pending状态,异步操作完成后转换为fulfilled或rejected状态。

Promise提供了then、catch、finally方法来注册回调函数,以处理异步操作的成功或失败结果。当Promise状态改变时,回调函数会被加入微任务队列等待执行,依赖事件循环机制在宏任务执行完成后优先执行微任务,从而保证回调函数的异步调度。

创建Promise对象

最基本的用法是通过构造函数实例化一个Promise对象,传入一个带有两个参数的函数,称为executor函数。executor函数接收两个参数:resolve和reject,分别表示异步操作成功和失败时的回调函数。

//创建了一个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回调函数并传递一个错误对象。

使用Promise对象

Promise对象创建后,可以使用then方法和catch方法指定fulfilled状态和rejected状态的回调函数。then方法可接受两个参数,一个处理fulfilled状态的函数,另一个处理rejected状态的函数。只传一个参数则表示当Promise对象状态变为fulfilled时,then方法会自动调用这个回调函数,并将Promise对象的结果作为参数传递给它。使用catch方法注册一个回调函数,用于处理“失败”的结果,即捕获Promise的状态改变为rejected状态或操作失败抛出的异常。Promise还可以使用finally注册回调函数,无论Promise最终状态如何(fulfilled或rejected),都会执行该回调函数。

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 方法定义成功和失败的回调
promise.then((result: number) => {
  console.info(`The number for success is ${result}`); // 成功时执行
}, (error: Error) => {
  console.error(error.message); // 失败时执行
}
);

// 使用 then 方法定义成功的回调,catch 方法定义失败的回调
promise.then((result: number) => {
  console.info(`Random number is ${result}`); // 成功时执行
}).catch((error: Error) => {
  console.error(error.message); // 失败时执行
});

// 无论成功还是失败都会执行
promise.finally(() => {
  console.info('finally complete');
})

说明

当Promise被reject且未通过catch方法处理时,会触发globalUnhandledRejectionDetected事件。可使用errorManager.on('globalUnhandledRejectionDetected')接口监听该事件,以全局捕获未处理的Promise reject。

async/await

async/await作用是将异步操作转同步执行。可以结合Promise/多线程(如TaskPool)使用。

关键字 主要作用
async 声明一个函数是异步函数。这个函数内部可以使用 await,并且其返回值会被自动包装成一个 Promise 对象。
await 在 async 函数内部使用,用于等待一个 Promise 对象的完成。它会暂停当前异步函数的执行,直到其后的 Promise 被解决(成功或失败),然后返回结果。
// 1. 使用 async 关键字定义一个异步函数
async function fetchUserData() {
  try {
    // 2. 在函数内部,使用 await 等待一个返回 Promise 的异步操作
    const response = await httpRequest.request(
      "https://api.example.com/user/1",
      { method: http.RequestMethod.GET }
    );
    // 3. 当 await 等待的 Promise 成功解决后,代码会继续向下执行
    console.log("请求结果:", response.result);
    return response.result; // 这个返回值会被自动包装为 Promise
  } catch (err) {
    // 4. 使用 try/catch 来捕获异步操作中可能发生的错误
    console.error("出错了:", err);
  }
}

// 5. 调用这个异步函数
fetchUserData();




import { taskpool } from '@ohos.taskpool';

async function handleUserClick() {
  // 1. UI线程快速响应,显示加载指示器
  showLoadingIndicator();
  
  // 2. 将重型CPU任务丢到taskpool(不会阻塞UI)
  const heavyTask = new taskpool.Task(heavyComputation, data);
  try {
    // 3. 等待taskpool在后台线程完成计算(逻辑等待,物理并行)
    const result = await taskpool.execute(heavyTask);
    // 4. 回到UI线程更新界面
    updateUIWithResult(result);
  } catch (error) {
    handleError(error);
  } finally {
    hideLoadingIndicator();
  }
}

emitter

在鸿蒙(HarmonyOS)应用开发中,Emitter 是一个进程内的事件处理机制,用于在同一进程的不同线程间或同一线程内部进行事件通信。它采用了常见的发布-订阅模式,让组件或模块之间能够方便地解耦通信。emitter官方API文档

在鸿蒙(HarmonyOS)应用开发中,使用Emitter进行事件通信时,直接在事件数据中定义方法是不可行的。事件数据在传递过程中需要被序列化,而方法(函数)无法被序列化。

不过,根据官方文档的说明,从API version 12开始,Emitter提供了泛型接口,允许你传递一个类实例,并在回调中调用该实例的方法。下面这个表格清晰地展示了这两种方式的区别:

特性 直接定义方法 (不可行) 传递类实例 (API 12+ 可行方案)
实现方式 直接在EventDatadata对象里定义方法 使用GenericEventData<T>data为泛型类型T的实例
数据序列化 ❌ 方法无法被序列化,导致传递失败 ✅ 传递的是类实例,在回调中可调用其方法
类型安全 ❌ 无类型约束 ✅ 提供泛型支持,类型更安全
适用场景 - 需要将数据和行为一同封装传递的复杂场景

如何传递与调用方法

虽然不能直接定义方法,但你可以通过传递类实例来达到调用方法的目的。以下是基于API version 12及以上版本的实现步骤和示例:

  1. 定义一个类:这个类包含你所需的数据以及方法。

  2. 使用泛型接口发送事件:发送事件时,使用emitter.emit<T>并指定泛型类型为你定义的类。事件数据data字段就是这个类的实例。

  3. 在接收方处理并调用方法:在订阅事件时,使用emitter.on<T>指定相同的泛型类型。在回调函数中,判断data是否为该类的实例,然后就可以安全地调用其方法了。

// 引入Emitter模块
import { emitter } from '@kit.BasicServicesKit';

// 1. 定义一个类,这个类需要被标记为@Sendable
@Sendable
class SampleData {
  count: number = 100; // 数据成员

  // 类的方法
  printCount(): void {
    console.info('Print count: ' + this.count);
  }
}

// 2. 发送事件的一方
function sendEventWithMethod() {
  // 创建类的实例
  let sampleInstance = new SampleData();
  sampleInstance.count = 200; // 可以设置数据

  // 使用泛型接口发送事件,指定泛型T为SampleData
  let eventData: emitter.GenericEventData<SampleData> = {
    data: sampleInstance // 将实例放入data字段
  };
  
  // 发送事件,eventId需与接收方一致
  emitter.emit("myEvent", eventData);
}

// 3. 接收事件的一方
// 订阅事件,同样使用泛型接口指定类型
let callback: Callback<emitter.GenericEventData<SampleData>> = (eventData: emitter.GenericEventData<SampleData>): void => {
  console.info(`Received event data.`);
  
  // 关键步骤:检查data是否为SampleData的实例
  if (eventData?.data instanceof SampleData) {
    // 如果是,则可以安全地调用其方法
    eventData.data.printCount(); // 将输出 "Print count: 200"
  }
};

// 注册事件监听
emitter.on("myEvent", callback);

注意事项

在实现过程中,有以下几个关键点需要特别注意:

  • 实例检查:在接收事件的回调函数中,必须使用instanceof来检查eventData.data是否是你的目标类实例。这是因为Emitter可能接收到不同结构的事件,进行类型判断可以保证代码的健壮性。

  • 生命周期管理:务必在组件或页面的生命周期结束时(例如aboutToDisappear中),使用emitter.off取消事件订阅,以防止内存泄漏。

  • emit()支持跨线程传输数据,但不支持@State@Observed等装饰器修饰的复杂类型。且需遵循线程间通信对象约束。

线程间通信对象约束

基于鸿蒙(HarmonyOS)ArkTS开发的线程间通信对象约束如下线程间通信对象概述

一、序列化通用约束

  1. 数据量限制
    单次序列化传输的数据量大小不得超过 16MB 1。

  2. 装饰器限制
    不支持序列化使用以下装饰器修饰的复杂类型: 1

    • @State
    • @Prop
    • @Link

二、对象类型约束

1. 普通对象

  • 支持传递基本数据类型(如 numberstring)。
  • 支持自定义对象,但需满足 序列化规范(即对象属性需为可序列化类型)。

2. ArrayBuffer对象

  • 适用于传递大段连续内存数据(如图片、音频原始数据)1。
  • 通过深拷贝实现二进制数据的高效传递。

3. SharedArrayBuffer对象

  • 支持多线程共享内存,允许线程间直接访问同一块内存区域1。
  • 注意事项:需避免多线程并发修改导致的数据竞争问题。

4. Transferable对象(如NativeBinding对象)1

  • 支持跨线程转移对象所有权(如文件描述符、图形资源)。
  • 转移后原线程 不再拥有访问权限

5. Sendable对象

  • 必须通过 @Sendable 装饰器显式标记。
  • 需满足 Sendable约束规则
    • 对象属性类型必须为基本类型、Sendable对象或不可变对象。
    • 禁止包含闭包或非Sendable的复杂类型。

三、通信行为约束

  1. 普通JS对象
    通过 Structured Clone算法 进行深拷贝(序列化与反序列化),效率较低2。

  2. SharedArrayBuffer
    直接共享内存,无深拷贝开销,但需自行管理线程安全。

  3. Transferable对象
    所有权转移后,原线程访问将抛出异常。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐