ArkTS 并发实战:TaskPool 与 Worker 的边界、选型与四个典型坑

举报
yd_260750576 发表于 2026/11/03 14:53:48 2026/11/03
【摘要】 一、先说结论:ArkTS 的并发不是"多线程随便开" HarmonyOS NEXT 的应用默认跑在一条主线程上:UI 渲染、事件分发、业务逻辑共享同一个事件循环。这条线程的帧预算大约是 16.7ms(60Hz)到 8.3ms(120Hz)。

一、先说结论:ArkTS 的并发不是"多线程随便开"

HarmonyOS NEXT 的应用默认跑在一条主线程上:UI 渲染、事件分发、业务逻辑共享同一个事件循环。这条线程的帧预算大约是 16.7ms(60Hz)到 8.3ms(120Hz)。一旦你在这条线程上做了一次几十毫秒的同步计算、一次同步文件读取,或者一次大 JSON 的解析,掉帧就是必然的——用户看到的是"划不动"。

ArkTS 给出的两种官方并发手段是 TaskPool 和 Worker。很多人第一反应是"哪个更强用哪个",但这两者的定位完全不同:

维度TaskPoolWorker
线程生命周期托管,任务结束线程复用手动创建/销毁,长期驻留
启动开销小(复用线程池)较大(每次实例化一条线程)
状态保持无状态,每次都是新任务可保持线程内状态
数量控制由框架按负载调度由开发者自己控制,容易开多
适用场景一次性、可并行的计算任务长期运行、需要累积状态的任务

一句话选型:能被切成"输入—计算—输出"的,用 TaskPool;需要一条常驻线程持续收消息、维护状态的,用 Worker。

二、TaskPool:把计算切成可并行的块

TaskPool 的核心约束是 @Concurrent 装饰器。只有被它标记的函数才能进入任务池执行,而这个装饰器有一串必须遵守的规则,踩中任何一条都会在编译期或运行期挂掉。

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

// 必须是顶层函数,不能是类方法
@Concurrent
function sumRange(start: number, end: number): number {
  let total = 0;
  for (let i = start; i < end; i++) {
    total += Math.sqrt(i);
  }
  return total;
}

async function runParallel(): Promise<number> {
  const tasks: taskpool.Task[] = [];
  const chunk = 1_000_000;
  for (let i = 0; i < 4; i++) {
    tasks.push(new taskpool.Task(sumRange, i * chunk, (i + 1) * chunk));
  }
  // 并发提交,全部完成后再汇总
  const results = await taskpool.execute(tasks) as number[];
  let total = 0;
  for (const r of results) {
    total += r;
  }
  return total;
}

这段代码里有三个值得注意的点:

  1. 参数是值传递。sumRange 拿到的 start、end 是拷贝过去的数字,不是引用。传对象时要按结构化克隆规则走,类和原型不会跟过去。
  2. taskpool.execute 支持传任务数组,框架会自动调度到不同线程并行执行,返回顺序与提交顺序一致——这比自己手写线程数控制要稳。
  3. 不要在 @Concurrent 函数里写 this。它不在原来的对象上下文里执行,this 是 undefined,闭包变量也一律拿不到。

再补两个实用能力:taskpool.executeDelayed 用于"延迟多久后跑一次",taskpool.executePeriodic 用于"每隔多久跑一次",后者返回的 Task 对象可以用 taskpool.cancel(task) 取消。周期性任务一定要持有 Task 引用,否则页面退出时你没法把它停掉,它会一直跑。

三、Worker:需要一条"活着"的线程

Worker 适合另一种形态:比如一个持续接收传感器数据、在内部维护滑动窗口的处理器,或者一个需要复用已加载模型/已解析词典的服务。这类任务的特点是"启动成本高、单次消息很小、状态要累积"。

// entry/ets/workers/DataWorker.ets
import { worker, ThreadWorkerGlobalScope, MessageEvents } from '@kit.ArkTS';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
let window: number[] = [];   // 线程内状态,跨消息保留

workerPort.onmessage = (e: MessageEvents): void => {
  const value = e.data as number;
  window.push(value);
  if (window.length > 100) {
    window.shift();
  }
  let sum = 0;
  for (const v of window) {
    sum += v;
  }
  workerPort.postMessage(sum / window.length);
};

主线程侧:

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

const w = new worker.ThreadWorker('entry/ets/workers/DataWorker.ets');
w.onmessage = (e: MessageEvent) => {
  console.info(`moving average = ${e.data}`);
};
w.postMessage(42);

// 页面销毁时必须显式终止,否则线程会一直活着
aboutToDisappear(): void {
  this.w?.terminate();
}

window 这个数组是 Worker 真正有价值的地方:它活在线程内部,跨消息保留,主线程完全不用管。用 TaskPool 做同样的事,你得每次把整个窗口传进去再传回来,序列化成本比计算本身还高。

四、四个典型坑

坑 1:在 @Concurrent 函数里访问外部变量。

const factor = 2;

@Concurrent
function scale(n: number): number {
  return n * factor;   // 编译不过 / 运行时拿不到
}

并发函数是独立编译单元,闭包不跨线程。正确做法是把 factor 作为参数传进去。

坑 2:往并发任务里传类实例,回来发现方法没了。

结构化克隆只复制可枚举的自有属性,原型链不在复制范围内。new User('a') 传过去,回来是个长得一样的普通对象,user.getName() 直接报 not a function。要么传纯数据对象,要么在接收侧重新构造实例,要么用 Sendable 对象(API 11 起支持,@Sendable 类可跨线程共享引用)。

坑 3:用 TaskPool 跑阻塞 IO。

任务池的线程数是有限的,被几个同步文件读写或者同步网络请求占住之后,后续计算任务只能排队。TaskPool 适合 CPU 密集型计算;IO 请走异步 API,或者干脆交给 Worker。

坑 4:高频小消息把 Worker 用成了"更慢的同步调用"。

postMessage 每次都要序列化。如果一帧内发几十条小消息,序列化和调度开销会超过计算本身,主线程反而更卡。正确做法是在主线程侧做批量合并(比如攒够 16ms 的数据一次发),或者改用 SharedArrayBuffer + Sendable,把"传数据"变成"共享内存"。

五、怎么验证自己真的优化了

不要凭感觉。至少做两件事:

  1. 在任务前后用 Date.now() 打点,配合 hilog 输出耗时,确认计算确实离开了主线程;
  2. 用 DevEco Studio 的 Profiler 看主线程的帧耗时分布,重点看有没有超过 16ms 的长帧。

如果优化后长帧还在,多半不是计算没搬走,而是搬走之后又在主线程上做了等结果之外的活——比如在 await 之后同步处理了几万条数据。把这段处理也一起塞进并发函数,才是完整的优化。

六、小结

TaskPool 和 Worker 不是替代关系:TaskPool 解决"把一次计算并行拆开",Worker 解决"给一类任务一条常驻线程"。真正决定性能的往往不是选哪个 API,而是有没有把序列化成本算进去、有没有把状态放对地方、有没有在页面退出时把线程收干净。

写并发代码时先问自己三个问题:这份数据要跨线程几次?这条线程要活多久?退出时谁来关掉它?想清楚这三个,剩下的 API 调用其实很少。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。