2. TaskPool任务池:基本概念、创建与执行任务、任务优先级管理
好,咱们今天聊聊 TaskPool。说实话,这个 API 是我在鸿蒙多线程开发里用得最多的一个。为什么?因为它把线程管理的脏活累活都替你干了。你想想看,如果每个异步任务都要你自己去 new Thread、管理生命周期、处理异常,那代码得多乱?
TaskPool 说白了就是一个「任务池子」。你把任务往里一扔,它自己调度线程去跑。我刚开始接触时也有个误区——以为它和 Java 的线程池是一回事。其实不完全一样,鸿蒙的 TaskPool 更轻量,而且和 UI 线程的交互更自然。
2.1 TaskPool 的核心概念
先理清几个关键概念:
- Task:你要执行的任务单元。可以是一个函数,也可以是一个闭包。
- TaskGroup:一组任务的集合。你可以批量提交,统一等待结果。
- Priority:优先级。高优先级的任务插队执行。
- Worker:真正干活的线程。TaskPool 内部维护了一组 Worker。
我记得有一次做图片批量处理应用,用户一次性选了 200 张图。如果每张图都开一个线程,系统直接卡死。后来改用 TaskPool,把任务分组,每组 10 张,配合优先级控制,体验瞬间丝滑。这就是 TaskPool 的价值——它帮你做资源管控。
核心原则:TaskPool 中的任务默认是并行的,但你可以通过 TaskGroup 控制并发粒度。不要一股脑把所有任务都扔进去,要根据设备性能调整。
2.2 创建与执行任务
创建任务很简单。你只需要定义一个函数,然后用 @Concurrent 装饰器标记一下。嗯,这里要注意:@Concurrent 是鸿蒙特有的装饰器,它告诉编译器这个函数可以在多线程环境下安全执行。
// 定义一个可并发执行的任务函数
@Concurrent
function processImage(data: ArrayBuffer): ArrayBuffer {
// 这里做图片处理,不会涉及 UI 操作
let result = new ArrayBuffer(data.byteLength);
// ... 处理逻辑
return result;
}
// 创建任务
let task = new taskpool.Task(processImage, imageData);
执行任务有两种方式:
- 直接执行:
taskpool.execute(task),返回 Promise。 - 批量执行:通过 TaskGroup 统一管理。
我个人习惯用第二种。为什么?因为批量执行时你可以统一捕获异常,还能控制任务间的依赖关系。来看个例子:
async function batchProcess() {
let group = new taskpool.TaskGroup();
for (let i = 0; i < 10; i++) {
let task = new taskpool.Task(processImage, images[i]);
group.addTask(task);
}
try {
let results = await taskpool.execute(group);
console.log('全部处理完成,结果数量:', results.length);
} catch (err) {
console.error('某个任务出错了:', err);
}
}
小技巧:如果你需要任务按顺序执行,可以在 TaskGroup 里设置 oneByOne 模式。不过这会损失并行性,慎用。
2.3 任务优先级管理
优先级这个东西,说实话,平时你可能觉得无所谓。但一旦遇到资源争抢,它就是救命稻草。鸿蒙的 TaskPool 支持三个优先级:
| 优先级 | 枚举值 | 适用场景 |
|---|---|---|
| HIGH | taskpool.Priority.HIGH | 用户交互响应、动画计算 |
| MEDIUM | taskpool.Priority.MEDIUM | 普通数据处理、网络请求 |
| LOW | taskpool.Priority.LOW | 后台日志、预加载、缓存清理 |
我曾经犯过一个错:把所有任务都设成 HIGH。结果呢?高优先级任务太多,反而互相抢占,谁都没跑快。优先级不是越高越好,而是要有差异化。
// 创建带优先级的任务
let highTask = new taskpool.Task(
processImage,
urgentImageData,
taskpool.Priority.HIGH
);
let lowTask = new taskpool.Task(
processImage,
cacheImageData,
taskpool.Priority.LOW
);
// 高优先级的任务会优先获得 Worker 资源
taskpool.execute(highTask);
taskpool.execute(lowTask);
避坑指南:我曾经在项目里把 UI 相关的计算任务设成 LOW,结果界面卡顿了好几次。记住:任何影响用户感知的任务,至少给 MEDIUM。HIGH 留给真正紧急的操作,比如触摸响应、动画帧计算。
2.4 任务的生命周期与取消
任务提交后,你还可以取消它。这个功能在用户快速操作时特别有用。比如用户连续点击「加载更多」,前一个请求还没回来,后一个又发出了。这时候取消旧任务就很有必要。
let task = new taskpool.Task(fetchData, params);
let execution = taskpool.execute(task);
// 用户又点了别的,取消当前任务
execution.cancel().then(() => {
console.log('任务已取消');
}).catch(err => {
console.error('取消失败,可能已经在执行中');
});
注意:取消不是万能的。如果任务已经开始执行,并且执行到了不可中断的代码段,取消可能不会立即生效。我建议在任务函数内部定期检查 taskpool.Task.isCanceled() 状态,主动退出。
2.5 实际项目中的使用建议
说了这么多,总结几条我踩坑后的经验:
- 任务粒度要适中:太细的任务(比如处理 1KB 数据)反而增加调度开销。我一般控制在 10ms 以上的任务才用 TaskPool。
- 不要在主线程创建大量 Task:创建 Task 本身也有开销。批量创建时用循环,但别在循环里做复杂计算。
- 优先级要动态调整:比如用户滑动列表时,把当前可见项的图片加载优先级提高,不可见的降低。
- 善用 TaskGroup:它不仅能批量执行,还能统一管理超时和取消。
一句话总结:TaskPool 是鸿蒙多线程开发的瑞士军刀。用好了,你的应用流畅得像抹了油;用不好,反而增加复杂度。多测试,多调优,找到最适合你场景的配置。
好,这一章就到这里。下一章我们会深入 TaskPool 的内部机制,看看它到底是怎么调度任务的。到时候我会分享一个我调试过的死锁案例,保证让你印象深刻。