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);

执行任务有两种方式:

  1. 直接执行taskpool.execute(task),返回 Promise。
  2. 批量执行:通过 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 的内部机制,看看它到底是怎么调度任务的。到时候我会分享一个我调试过的死锁案例,保证让你印象深刻。