HarmonyOS 并发编程实战:TaskPool 与 Worker 的选型、通信与避坑

前言

HarmonyOS 主线程(UI 线程)负责渲染与交互,一旦执行耗时计算(大图解码、JSON 批量解析、加密运算、文件压缩)就会掉帧、卡 UI。ArkTS 并发模型提供两条主力路径:TaskPool(任务池)Worker(长期工作线程)。两者都是基于 Actor 模型的「线程 + 消息通信」,但适用场景截然不同。本文用可运行示例讲清:什么时候用 TaskPool、什么时候用 Worker、线程间怎么传数据、以及最常见的「序列化失败」「上下文丢失」等坑。

问题描述

开发者常踩的坑:

  1. 把一个本来只需跑几百毫秒的计算丢进 Worker,结果「建线程的开销比计算还大」。
  2. 想给 Worker 传一个类实例,结果收到的是 undefined——因为 ArkTS 线程间只传可序列化数据
  3. 在 TaskPool 任务里调用了 UI 相关 API(如 promptAction),直接抛异常。
  4. 忘记 worker.terminate(),后台堆积十几个 Worker 拖垮内存。

核心要分清:TaskPool 适合「一次性、无状态、可取消的短任务」;Worker 适合「长生命周期、有状态、需常驻的会话」

细节解析

1. 两种模型对比

维度TaskPoolWorker
生命周期任务结束即回收,自动复用线程池需手动 terminate()
状态无状态,不能持有全局变量可维护自身状态
任务数量理论无上限(自动调度)最多 8 个(含主线程限制)
取消支持 taskpool.cancel()需自行实现退出信号
典型场景图片处理、加解密、排序长连监听、持续采集、音视频

2. 线程间通信的数据约束

  • 只能传可序列化对象:基础类型、Array、TypedArray、ArrayBuffer(需 transferList 转移所有权)、实现了 collections 容器的对象等。
  • 函数、@ObservedV2 实例、包含方法的类实例不能直接传。需要传「数据」而非「行为」。

3. ArrayBuffer 的所有权转移

通过 taskpool.Task 的第三个参数 transferListArrayBuffer 的所有权转移给工作线程,避免拷贝大内存,但转移后主线程那份立即失效。

4. 全局对象限制

Worker / TaskPool 环境没有 UI 上下文,不能调用 windowdocumentpromptAction 等;也不能访问主线程的模块级单例。

示例代码(可运行 ArkTS/ArkUI)

示例 1:TaskPool 执行一个耗时计算

// utils/heavy.ets —— 注意:这是工作线程侧代码,不要引用 UI 模块
import { taskpool } from '@kit.ArkTS';

@Concurrent
function fibHeavy(n: number): number {
  // 模拟耗时递归
  if (n < 2) return n;
  return fibHeavy(n - 1) + fibHeavy(n - 2);
}

// 主线程侧调用
async function runByTaskPool(): Promise<number> {
  const task = new taskpool.Task(fibHeavy, 38);
  return await taskpool.execute(task) as number;
}
// pages/Home.ets
import { runByTaskPool } from '../utils/heavy';

@Entry
@ComponentV2
struct Home {
  @Local result: number = 0;
  @Local loading: boolean = false;

  build() {
    Column({ space: 16 }) {
      Button(this.loading ? '计算中…' : 'TaskPool 计算')
        .enabled(!this.loading)
        .onClick(async () => {
          this.loading = true;
          // 放到后台线程,UI 不卡
          this.result = await runByTaskPool();
          this.loading = false;
        })
      Text(`结果:${this.result}`).fontSize(18)
    }
    .padding(20)
  }
}

示例 2:可取消的任务

import { taskpool } from '@kit.ArkTS';

@Concurrent
function countTo(n: number, progress: taskpool.TaskCallBack): void {
  for (let i = 0; i < n; i++) {
    progress.updateProgress?(i / n);
  }
}

async function runCancellable() {
  const task = new taskpool.Task(countTo, 1_000_000);
  const exec = taskpool.execute(task, taskpool.Priority.HIGH);
  // 中途取消
  setTimeout(() => taskpool.cancel(task), 50);
  try {
    await exec;
  } catch (e) {
    console.info('任务已取消:', e);
  }
}

示例 3:Worker 长生命周期会话

// workers/keepAlive.ets —— Worker 脚本
import worker from '@ohos.worker.worker';

const parent = worker.workerPort;

parent.onmessage = (e: MessageEvents) => {
  const { type, payload } = e.data;
  if (type === 'echo') {
    // 工作线程可维护自己的状态
    parent.postMessage({ type: 'result', payload: payload * 2 });
  }
};
// 主线程
import { worker } from '@kit.ArkTS';

@Entry
@ComponentV2
struct WorkerDemo {
  private wk: worker.ThreadWorker | null = null;

  aboutToAppear() {
    this.wk = new worker.ThreadWorker('entry/ets/workers/keepAlive.ets');
    this.wk.onmessage = (e) => { console.info('来自 Worker:', JSON.stringify(e.data)); };
    this.wk.postMessage({ type: 'echo', payload: 21 });
  }

  aboutToDisappear() {
    this.wk?.terminate(); // 务必释放
    this.wk = null;
  }

  build() { Column() { Text('Worker 示例') } }
}

总结

  • 短、碎、可取消 → TaskPool@Concurrent 函数 + taskpool.execute),零管理成本。
  • 长、有状态、需常驻 → Worker,但记得 terminate(),且数量不超过 8 个。
  • 传数据不传行为:线程间只传可序列化对象,函数/类实例要靠「主线程发数据 → 工作线程执行内置逻辑」的模式。
  • 大内存用 transferList 转移所有权,避免拷贝;转移后原线程那份失效。
  • 工作线程无 UI 上下文,任何 UI API 调用都会抛异常,把计算留在后台、把结果回传主线程渲染。

合理并发化后,原本 300ms 的卡顿可以完全从主线程卸载,用户感知到的就是「丝滑」。

Logo

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

更多推荐