系列文章《鸿蒙开发从入门到专家》第 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(),否则线程会一直占用资源。
四、选型对比
一个常见误区是:把所有耗时操作都往 Worker 里塞。其实 Worker 有创建开销,短任务用 TaskPool 更轻量;长任务或需要持续交互的才用 Worker。
五、实战:批量图片缩略图生成
假设相册应用需要在后台把 100 张原图生成缩略图。合理做法:
主线程把路径列表拆成 4 个批次。
每个批次交给 TaskPool 并行处理。
处理结果通过 Promise.all 汇总。
生成过程中用
@State更新进度条,UI 保持响应。
如果图片处理逻辑需要复用解码器状态,或者要实时返回进度,那就换成 Worker,每处理完一张发一条消息回主线程。
小结
并发编程的本质不是“多线程炫技”,而是把主线程解放出来。记住两条:
短、可并行的计算 → TaskPool。
长、需持续通信的后台任务 → Worker。
两者都不要共享可变状态,数据走序列化或 ArrayBuffer 转移。
下一篇我们进入数据层进阶,看看鸿蒙关系型数据库 RDB 怎么做高级查询和索引优化。