Administrator
发布于 2026-09-01 / 52 阅读
0
0

鸿蒙并发编程:TaskPool、Worker 与异步模式选型

系列文章《鸿蒙开发从入门到专家》第 22 篇 · 中级

上一篇我们在 Canvas 动画里反复提到一个原则:别让绘制阻塞主线程。其实整个鸿蒙应用开发都围绕这个原则展开。ArkTS 是单线程事件循环模型,UI 渲染、用户输入、业务回调都在同一条主线程上跑。一旦某个任务耗时过长,界面就会卡顿甚至 ANR。

鸿蒙提供了两套并发方案:TaskPool 和 Worker。名字听起来像,适用场景却不一样。本文把它们的边界、用法和常见误区说清楚。

一、TaskPool:适合可拆分的短时计算任务

TaskPool 是一个托管线程池,开发者只需要把任务函数丢进去,系统会自动调度线程。它的特点是:

  • 线程由系统管理,自动复用。

  • 任务之间不能共享内存,数据通过序列化传递。

  • 适合 CPU 密集型、可并行的计算,比如图片压缩、大数据解析、批量加密。

基本用法:

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

@Concurrent
function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

async function runTask() {
  const task = new taskpool.Task(fibonacci, 35);
  const result = await taskpool.execute(task);
  console.info(`fib(35) = ${result}`);
}

注意 @Concurrent 装饰器。它告诉编译器这个函数可以在并发线程里执行。没有它,TaskPool 会报错。

TaskPool 还有一个高级用法:任务组(TaskGroup)。把多个独立任务放进一个组,可以并行执行并统一等待结果:

const group = new taskpool.TaskGroup();
for (let i = 0; i < 4; i++) {
  group.addTask(new taskpool.Task(heavyCompute, i));
}
const results: number[] = await taskpool.execute(group);

这里要注意数据传递成本。如果任务输入输出都是大对象,序列化和反序列化本身会消耗时间。 TaskPool 真正划算的场景是“计算量远大于传输量”。

二、Worker:适合需要持续通信的后台任务

Worker 是一颗独立线程,生命周期由开发者控制。和 TaskPool 最大的区别是:Worker 可以持续运行,并且通过消息机制与主线程双向通信。

典型场景包括:

  • 后台播放器的音频解码

  • 长时间的数据同步

  • 需要状态保持的计算服务

创建 Worker:

// entry/src/main/ets/workers/ImageWorker.ets
import { worker } from '@kit.ArkTS';

const workerPort = worker.workerPort;

workerPort.onmessage = (event: MessageEvents) => {
  const { id, buffer } = event.data;
  // 模拟耗时处理
  const result = processImage(buffer);
  workerPort.postMessage({ id, result });
};

主线程调用:

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

const imageWorker = new worker.ThreadWorker('entry/ets/workers/ImageWorker.ets');

imageWorker.onmessage = (event) => {
  console.info(`处理完成:${event.data.id}`);
};

imageWorker.postMessage({ id: 1, buffer: rawData });

Worker 的通信数据也受序列化限制,不能传递复杂对象引用。如果需要共享大段内存,可以用 ArrayBuffer 配合 transferList 转移所有权,避免拷贝。

三、Promise 与 async/await:把异步代码写顺

无论是 TaskPool 还是 Worker,最终都要通过 Promise 或回调拿到结果。ArkTS 支持标准的 async/await,建议把所有并发调用都包装成 Promise,避免回调嵌套。

function compressImage(filePath: string): Promise<string> {
  return new Promise((resolve, reject) => {
    const w = new worker.ThreadWorker('entry/ets/workers/CompressWorker.ets');
    w.onmessage = (e) => resolve(e.data);
    w.onerror = (err) => reject(err);
    w.postMessage({ path: filePath });
  });
}

async function uploadBatch(paths: string[]) {
  const tasks = paths.map(p => compressImage(p));
  const results = await Promise.all(tasks);
  return results;
}

一个小建议:在 Worker 或 TaskPool 任务完成后,如果是临时 Worker,记得调用 terminate(),否则线程会一直占用资源。

四、选型对比

维度

TaskPool

Worker

线程管理

系统托管

开发者创建/销毁

典型生命周期

短,一次任务

长,持续服务

双向通信

不支持

支持

适用场景

批量计算、可并行任务

后台服务、长连接、状态保持

数据传递

序列化

序列化 / ArrayBuffer 转移

一个常见误区是:把所有耗时操作都往 Worker 里塞。其实 Worker 有创建开销,短任务用 TaskPool 更轻量;长任务或需要持续交互的才用 Worker。

五、实战:批量图片缩略图生成

假设相册应用需要在后台把 100 张原图生成缩略图。合理做法:

  1. 主线程把路径列表拆成 4 个批次。

  2. 每个批次交给 TaskPool 并行处理。

  3. 处理结果通过 Promise.all 汇总。

  4. 生成过程中用 @State 更新进度条,UI 保持响应。

如果图片处理逻辑需要复用解码器状态,或者要实时返回进度,那就换成 Worker,每处理完一张发一条消息回主线程。

小结

并发编程的本质不是“多线程炫技”,而是把主线程解放出来。记住两条:

  • 短、可并行的计算 → TaskPool。

  • 长、需持续通信的后台任务 → Worker。

  • 两者都不要共享可变状态,数据走序列化或 ArrayBuffer 转移。

下一篇我们进入数据层进阶,看看鸿蒙关系型数据库 RDB 怎么做高级查询和索引优化。


评论