鸿蒙线程间通信怎么选:TaskPool、TaskGroup、LongTask 与 Worker 实战

一、问题背景

主线程承担界面渲染、用户交互和生命周期分发。如果把图片批处理、批量数据转换、数据库查询或持续监听直接放在主线程,任务执行期间就可能挤占帧渲染时间,表现为卡顿、掉帧或点击无响应。

把任务放到子线程只是第一步。真正容易出问题的是"结果怎么回来":是任务结束后一次性返回,还是中途持续汇报?任务结束后线程是否可以回收?多个任务是分别处理,还是必须等全部完成?

先给结论:

业务特征 推荐能力 返回方式 资源管理重点
一次性耗时任务,结束后只要结果 TaskPool + Task Promise 任务结束后由系统回收空闲线程
多个独立任务,必须汇总全部结果 TaskGroup Promise<Array<T>> 任务组只执行一次,统一处理结果
任务执行中需要同步上报进度 Task + sendData onReceiveData + 最终结果 sendData 必须在并发函数执行期调用
长时间监听、回调或流式任务 LongTask + emitter 事件回调 结束时取消监听并 terminateTask
一个长期存在、可接收多种指令的线程 Worker postMessage/onmessage 页面退出或业务结束时 terminate

二、前置条件

1. 版本与 API 边界

  • 示例使用 ArkTS 和 @kit.ArkTS,适用于 API 11 及以上的线程通信案例。
  • Task.sendData()Task.onReceiveData() 从 API 11 开始支持。
  • taskpool.LongTasktaskpool.terminateTask() 从 API 12 开始支持。
  • Worker 的基础能力从 API 9 开始支持;API 18 及以上建议注册 onAllErrors 捕获 Worker 生命周期内的异常。
  • 代码片段只展示并发模块和页面通信关键路径,工程仍需使用与目标设备匹配的 DevEco Studio、HarmonyOS SDK 和签名配置。

2. 线程边界

ArkTS 的并发实例之间默认是内存隔离的。普通对象跨线程传递时会发生序列化,不能把页面组件、控制器或带有 UI 装饰器的对象直接塞进任务函数。并发函数应放在独立的非 UI 文件中,参数和返回值使用可序列化的数据、ArrayBuffer 或经过验证的 Sendable 对象。

@ComponentV2 页面只负责发起任务、接收结果和更新状态;耗时计算函数不要反向导入页面文件。这样既避免子线程解析 UI 装饰器,也让任务更容易单测和复用。

三、核心内容

1. 一次性任务:用 TaskPool + Task 返回最终结果

适合图片处理、数据转换、一次性查询等"输入明确、执行结束后只返回一次结果"的任务。并发函数需要使用 @Concurrent,并保持在模块顶层,不能依赖页面组件的 this

typescript 复制代码
// task-functions.ets:只放纯计算逻辑,不导入 ArkUI 页面模块
import { taskpool } from '@kit.ArkTS';

@Concurrent
export function sumRange(start: number, end: number): number {
  let total: number = 0;
  for (let value: number = start; value <= end; value++) {
    total += value;
  }
  return total;
}

export async function executeOnce(): Promise<number> {
  const task: taskpool.Task = new taskpool.Task(sumRange, 1, 100000);
  // execute 的结果回到宿主线程,再由页面提交到 @Local 状态。
  return await taskpool.execute(task) as number;
}

这里的 Promise 回调仍然运行在宿主线程。不要因为任务已经放到 TaskPool,就在 thenawait 后继续做大规模数据加工;结果回传后的整理和 UI 更新同样可能阻塞页面。

2. 多个任务:用 TaskGroup 做统一汇总

当多个任务相互独立,但页面必须等它们全部完成后再展示结果,可以把任务加入 TaskGroup。任务组的返回数组与添加任务的顺序对应,页面只需要处理一次汇总结果。

typescript 复制代码
import { taskpool } from '@kit.ArkTS';

export async function executeGroup(): Promise<Array<number>> {
  const group: taskpool.TaskGroup = new taskpool.TaskGroup();
  group.addTask(new taskpool.Task(sumRange, 1, 50000));
  group.addTask(new taskpool.Task(sumRange, 50001, 100000));
  group.addTask(new taskpool.Task(sumRange, 100001, 150000));

  // TaskGroup 只适合一次性汇总,不要加入 LongTask 或监听型任务。
  return await taskpool.execute(group) as Array<number>;
}

如果数据量很大,可以先切分输入,再用任务组并发处理。切分粒度不要只看 CPU 核数,还要结合任务本身的计算量、序列化成本和返回结果大小做性能测试。短到几乎没有计算量的任务,放入 TaskPool 反而可能增加调度和通信开销。

3. 任务中途回传:sendData 只用于同步执行期

sendData 适合进度、阶段性计数或分段结果。宿主线程先注册 onReceiveData,并发函数再调用 taskpool.Task.sendData()

typescript 复制代码
import { taskpool } from '@kit.ArkTS';

@Concurrent
export function sumWithProgress(totalCount: number): number {
  let total: number = 0;
  for (let value: number = 1; value <= totalCount; value++) {
    total += value;
    if (value % 1000 === 0) {
      // 必须发生在并发函数的执行期,不能延后到异步回调中。
      taskpool.Task.sendData(value);
    }
  }
  return total;
}

export async function executeWithProgress(
  onProgress: (value: number) => void
): Promise<number> {
  const task: taskpool.Task = new taskpool.Task(sumWithProgress, 10000);
  task.onReceiveData(onProgress);
  return await taskpool.execute(task) as number;
}

原案例中特别提醒:不要在 Promise.then()、事件回调或其他异步回调里直接调用 SendData()。任务函数可能已经返回,Task 的生命周期也可能已经结束,此时消息不一定能回到宿主线程。需要长期监听或异步回调时,使用下一节的 LongTask + emitter

4. 长时间监听:LongTask + emitter,结束时主动释放

对 HTTP 监听、传感器监听、持续下载或流式处理,不要把监听注册在普通 Task 中。普通任务返回后,TaskPool 可能回收空闲线程;监听回调稍后再次触发时,就会出现线程已回收、回调失效甚至崩溃的风险。

官方案例推荐把监听放入 LongTask,用 emitter 做宿主线程和任务线程之间的双向通知。停止流程必须成对处理:先发停止事件,再取消事件监听,最后调用 terminateTask

typescript 复制代码
import { emitter } from '@kit.BasicServicesKit';
import { taskpool } from '@kit.ArkTS';

const DATA_EVENT_ID: number = 3001;
const STOP_EVENT_ID: number = 3002;

@Concurrent
export async function longTaskBody(): Promise<void> {
  let timerId: number = 0;
  emitter.on({ eventId: STOP_EVENT_ID }, () => {
    clearInterval(timerId);
    emitter.off(STOP_EVENT_ID);
  });

  timerId = setInterval(() => {
    // 真实场景可在这里转发传感器、网络或流式数据。
    emitter.emit({ eventId: DATA_EVENT_ID });
  }, 1000);
}

export function startLongTask(): taskpool.LongTask {
  const task: taskpool.LongTask = new taskpool.LongTask(longTaskBody);
  emitter.on({ eventId: DATA_EVENT_ID }, () => {
    // 宿主线程在这里更新业务状态;页面销毁时必须取消该监听。
  });
  taskpool.execute(task).catch(() => {
    stopLongTask(task);
  });
  return task;
}

export function stopLongTask(task: taskpool.LongTask): void {
  emitter.emit({ eventId: STOP_EVENT_ID });
  emitter.off(DATA_EVENT_ID);
  taskpool.terminateTask(task);
}

上例用定时器模拟持续数据源,真实业务中应替换成具体监听 API,并在停止事件里调用对应的 offclosereleaseterminateTask 不是替代业务清理的"强制按钮",监听器、定时器、文件句柄和网络请求仍要由业务代码主动关闭。

5. 多种指令和常驻线程:用 Worker

Worker 更适合一个线程长期存在,并按消息执行多种任务的场景。例如编辑器后台解析、持续文件扫描、下载和解压流水线。宿主线程通过 postMessage 发消息,Worker 通过 workerPort.onmessage 接收并用 postMessage 返回结果。

主线程页面使用 V2 装饰器:

typescript 复制代码
// WorkerPage.ets
import { worker, ErrorEvent, MessageEvents } from '@kit.ArkTS';

interface WorkerRequest {
  type: string;
  values: Array<number>;
}

interface WorkerResponse {
  type: string;
  text: string;
}

@Entry
@ComponentV2
struct WorkerPage {
  @Local status: string = '等待任务';
  private workerInstance: worker.ThreadWorker | undefined = undefined;

  aboutToAppear(): void {
    this.workerInstance = new worker.ThreadWorker('entry/ets/workers/Worker.ets');
    this.workerInstance.onmessage = (event: MessageEvents): void => {
      const response: WorkerResponse = event.data as WorkerResponse;
      if (response.type === 'result') {
        this.status = response.text;
      }
    };
    this.workerInstance.onAllErrors = (error: ErrorEvent): void => {
      this.status = `Worker异常:${error.message}`;
    };
  }

  aboutToDisappear(): void {
    this.workerInstance?.terminate();
    this.workerInstance = undefined;
  }

  build(): void {
    Column() {
      Text(this.status)
        .fontSize(18)
      Button('提交计算任务')
        .margin({ top: 16 })
        .onClick(() => {
          const request: WorkerRequest = {
            type: 'sum',
            values: [1, 2, 3, 4, 5]
          };
          this.workerInstance?.postMessage(request);
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

Worker 文件只处理消息和计算,不导入页面:

typescript 复制代码
// entry/ets/workers/Worker.ets
import { MessageEvents, worker } from '@kit.ArkTS';

interface WorkerRequest {
  type: string;
  values: Array<number>;
}

interface WorkerResponse {
  type: string;
  text: string;
}

const workerPort = worker.workerPort;

workerPort.onmessage = (event: MessageEvents): void => {
  const request: WorkerRequest = event.data as WorkerRequest;
  if (request.type !== 'sum') {
    return;
  }

  let total: number = 0;
  for (let index: number = 0; index < request.values.length; index++) {
    total += request.values[index];
  }

  const response: WorkerResponse = {
    type: 'result',
    text: `计算结果:${total}`
  };
  workerPort.postMessage(response);
};

页面退出时调用 terminate(),否则 Worker 仍可能持有消息队列和定时器。API 18 及以上优先使用 onAllErrors;如果项目兼容更低 API,需要根据目标版本改用 onerror,并注意 onerror 捕获范围更窄。

四、代码与验证

1. 建议的工程拆分

text 复制代码
entry/src/main/ets/
├── pages/
│   └── WorkerPage.ets          # @Entry、@ComponentV2、@Local、生命周期
├── tasks/
│   ├── TaskPoolTasks.ets       # @Concurrent 纯计算函数
│   └── LongTaskTasks.ets       # LongTask 与 emitter 的业务任务
└── workers/
    └── Worker.ets              # workerPort.onmessage 与消息模型

拆分后有三个直接收益:

  1. 子线程不需要解析页面文件和 UI 装饰器。
  2. 任务参数、返回值和消息模型集中定义,序列化边界更清晰。
  3. 页面销毁逻辑集中在 aboutToDisappear,不容易遗留 Worker 或长时任务。

2. 验证路径

  1. 先验证一次性任务:确认 executeOnce() 结果能回到宿主线程,并且页面状态只在宿主线程更新。
  2. 再验证进度回传:确认 onReceiveData 在最终 Promise 结果之前能收到阶段性消息。
  3. 验证异常路径:主动传入不可序列化或错误数据,确认 Promise 的 catch 能捕获任务执行异常;taskpool.execute() 的参数序列化失败则要在调用侧使用外层 try...catch
  4. 验证页面退出:在任务执行中返回上一页,确认 LongTask 已停止、Worker 已 terminate,事件监听和定时器已取消。
  5. 使用 DevEco Profiler 或 HiTrace 对比主线程帧渲染、任务执行、结果回调和 UI 更新耗时。重点检查结果回调是否一次性处理了过大的数组。

五、踩坑与注意事项

1. 不要把"异步"误当成"自动多线程"

async/awaitPromise 只改变异步组织方式,不会自动把 CPU 密集型代码挪到子线程。需要并行计算时,仍要显式使用 TaskPool 或 Worker。

2. 不要在子线程导入 UI 模块

即使没有直接调用页面组件,只要导入链中解析到 V1/V2 UI 装饰器、页面状态或 ArkUI 组件,就可能导致子线程解析失败。任务文件应遵循最小化导入原则。

3. 不要在异步回调中调用 sendData

sendData 依赖当前 Task。任务函数返回后再从 then、定时器或其他回调发送消息,存在 Task 已结束的生命周期问题。持续回调使用 emitter;持续线程使用 Worker。

4. 不要把 TaskGroup 当作常驻调度器

TaskGroup 适合一次性的并行任务汇总,不能加入 LongTask、周期任务或已经执行过的任务。需要按消息不断接收新指令时,应使用 Worker,或者在宿主线程重新创建新的 Task。

5. 任务结果返回后仍可能卡 UI

子线程只负责把工作移出主线程。结果回到宿主线程后,数组合并、JSON 转换、复杂 forEach 和大量组件刷新仍然发生在主线程。必要时分批提交状态,避免一次性刷新过大的 UI 数据集。

6. 资源释放必须和创建成对

setInterval 对应 clearInterval,事件监听对应 off,HTTP 或数据库句柄对应 close/release,Worker 对应 terminate,LongTask 对应 terminateTask。仅等待 Promise 完成,不等于所有监听和句柄都被释放。

六、结论

线程通信的选择不应从 API 名称开始,而应从任务生命周期开始:

  • 只要最终结果:TaskPool + Task
  • 多个结果一起用:TaskGroup
  • 执行中同步报进度:sendData + onReceiveData,但只在并发函数执行期发送。
  • 长期监听:LongTask + emitter,主动管理监听和任务生命周期。
  • 长期驻留、消息驱动、多种指令:Worker,页面退出时 terminate

把 UI、任务函数、Worker 消息模型和释放逻辑分开,线程通信才不会从"主线程卡顿"演变成"线程泄漏、回调失效和状态错乱"。

七、参考资料

  1. HarmonyOS-Cases:线程间通信场景
  2. TaskPool API 参考
  3. Worker API 参考
  4. TaskPool 与主线程通信指导(API 参考版本)
  5. ArkTS 线程间通信示例:ThreadCommunication
相关推荐
woshihuanglaoshi3 小时前
错题四科入库:鸿蒙错题本种子数据与复习队列效果
学习·华为·harmonyos·鸿蒙
kiros_wang4 小时前
鸿蒙ArkTS枚举实战|静态枚举、动态枚举业务选型、规范落地与避坑全解
harmonyos
2501_919749036 小时前
华为鸿蒙免费音乐APP—小羊免费音乐
华为·harmonyos·鸿蒙
世人万千丶6 小时前
物品借还闭环:鸿蒙物品清单种子数据与清单效果
学习·华为·harmonyos·鸿蒙
贾伟康7 小时前
【知律|10】HarmonyOS ArkTS 案例边界实战:明确普法内容不替代法律意见
harmonyos·arkts·arkui·应用合规·内容治理
xq95277 小时前
鸿蒙组件化设计横空出世
harmonyos
特立独行的猫A7 小时前
Tauri v2 桌面应用m3u8dl-tauri移植到 HarmonyOS(鸿蒙 PC)完整实战指南
harmonyos
HarmonyOS_SDK7 小时前
从“一屏一态”到“一屏多能”:WPS通过HarmonyOS多窗口能力重塑移动办公体验
harmonyos