TaskPool vs Worker:一张表帮你选对并发方案

写并发代码时最常被问的就是"这个场景用 TaskPool 还是 Worker"。两者都是基于 Actor 模型的多线程方案,能力上有重叠,定位上有差异。TaskPool 在 Worker 之上做了调度器和线程池的封装,自动管生命周期;Worker 给你一个独立 Runtime,自由度高但要自己管。这篇把两者从实现特点、工作原理、适用场景、具体实践四个维度做一次完整对比,最后给一个选型决策树。

实现特点对比

直接上表,20+ 维度逐项对比:

维度TaskPoolWorker
线程运行环境基于 Worker 线程池,调度器统一管理每个 Worker 独立内存空间、EventLoop、CallStack
内存模型线程间隔离,内存不共享线程间隔离,内存不共享
通信方式调度器序列化分发任务,Promise-Then 返回主子线程 Message 收发
参数传递机制Structured Clone 序列化,支持 ArrayBuffer 转移、SharedArrayBuffer 共享、Sendable 引用传递同 TaskPool
参数传递直接传递,无需封装消息对象为唯一参数,需自己封装
方法调用直接传 @Concurrent 修饰的方法在 Worker 线程里解析消息调用对应方法
返回值异步调用后默认返回主动 postMessage,需在 onmessage 里解析
调度机制内置调度器,优先级 + 防饥饿算法无调度器,开发者自行管理
线程池管理自动扩缩容不支持,自行创建销毁
生命周期自动管理手动管理
个数上限自动管理,无需配置同进程最多 64 个,实际由内存决定
执行时长上限3 分钟(不含异步 I/O 等待),LongTask 无限制无限制
任务优先级支持API 18 起支持
任务取消支持不支持
线程复用支持不支持
任务延时执行支持不支持
任务依赖关系支持不支持
串行队列支持不支持
任务组支持不支持
周期任务支持不支持
异步队列支持不支持

TaskPool 支持的能力比 Worker 多很多,这些都是调度器封装带来的。Worker 没有调度器,所以优先级、取消、任务组、延时、依赖这些都没法做。

工作原理对比

两者都基于 Actor 并发模型,但架构层次不同。

Worker 工作原理

每个 Worker 线程和主线程一样有完整的 Runtime:内存空间、MessageQueue、EventLoop、CallStack。线程之间通过 Message 交互。

Worker线程2

Worker线程1

主线程

postMessage

postMessage

postMessage

postMessage

CallStack

EventLoop

Memory

CallStack

EventLoop

Memory

CallStack

EventLoop

Memory

多核 CPU 上多个 Worker 线程可以真正并行。但 Worker 之间不能直接通信,必须通过主线程中转。

TaskPool 工作原理

TaskPool 在 Worker 之上加了调度器和线程池。主线程调 execute() 把任务按优先级放进 TaskQueue,调度器按优先级 + 防饥饿算法取任务,序列化后丢给 Worker 线程池,工作线程执行后反序列化结果,以 Promise-Then 返回主线程。线程池根据任务数量和执行时间自动扩缩容。

主线程 execute

TaskQueue
按优先级排队

调度器
优先级 + 防饥饿

序列化

Worker 线程池
自动扩缩容

工作线程执行

反序列化

Promise-Then 返回主线程

关键差异:TaskPool 多了调度器和线程池两层,所以它支持优先级、取消、任务组这些能力,但也因此多了一些内存开销。

适用场景对比

TaskPool 工作线程绑定系统调度优先级,支持负载均衡。Worker 要自己创建销毁,有创建和管理成本。大多数场景优先 TaskPool。

适合 Worker 的场景

运行时间超过 3 分钟的任务。 这里的 3 分钟指同步执行时长,不含 Promise/async-await 异步 I/O 等待。比如后台跑 1 小时的预测算法训练,CPU 密集型,必须用 Worker。

有强关联的一系列同步任务。 比如要创建并使用句柄的场景,每次创建的句柄都不同,且必须持续保存该句柄确保后续操作正确执行。这种依赖线程上下文的场景,Worker 的独立 Runtime 正好合适。

对运行时内存占用敏感的场景。 TaskPool 多了调度器和线程池,任务多时多占一些内存。设备内存受限或任务量大时,Worker 内存占用更低。

适合 TaskPool 的场景

需要设置任务优先级的任务。 API 18 之前 Worker 不支持调度优先级。比如图像直方图绘制,后台计算的直方图数据要给前台显示,影响用户体验,任务相对独立,用 TaskPool 配 HIGH 优先级。

需要频繁取消的任务。 比如大图浏览场景,缓存当前图片左右各两张。滑到下一张时要取消另一侧的缓存任务,TaskPool 支持取消,Worker 不支持。

大量或调度点分散的任务。 大型应用多个模块都有耗时任务,用 Worker 难以做负载管理,TaskPool 自动调度更合适。

具体实践对比:图片编辑场景

以 ArkTS 图片编辑场景为例,从编码效率、线程创建耗时、数据传输、任务执行耗时、内存占用五个维度对比。实验环境:8 核手机,中载(CPU 占用 50%~60%)和重载(CPU 占用 90%+)两种。

编码效率

Worker 方案 要开发者自己控制 Worker 实例数量(最多 64 个),复用线程,及时销毁。基本流程:

// 1. 根据任务数创建 Worker,最多 64 个
const taskNum = 14;
const curTaskNum = taskNum <= 64 ? taskNum : 64;
const workers: worker.ThreadWorker[] = [];
for (let i = 0; i < curTaskNum; i++) {
  workers.push(new worker.ThreadWorker(WorkerName));
}

// 2. 拆分图片像素数据,分配给 Worker
const buffers = splitArrayBuffer(bufferArray, taskNum);
for (let i = 0; i < taskNum; i++) {
  workers[i].postMessage(new MessageItem(buffers[i], sliderValue, value));
}

// 3. 接收结果,复用 Worker 处理剩余任务,全部完成后销毁
workers[index].onmessage = (e: ESObject) => {
  newBuffers[e.data.index] = e.data.buffer;
  if (allocation !== 0) {
    workers[index].postMessage(messages[n++]); // 复用
    allocation--;
  } else if (num === taskNum) {
    for (let i = 0; i < curTaskNum; i++) {
      workers[i].terminate(); // 销毁
    }
    updatePixelMap(mergeArrayBuffers(newBuffers));
  }
};

TaskPool 方案 用 TaskGroup 把大任务拆成小任务丢进任务组:

private async execImageProcessing(buffer: ArrayBuffer, type: AdjustId, value: number): Promise<ArrayBuffer> {
  const buffers = splitArrayBuffer(buffer, 240);
  const group = new taskpool.TaskGroup();
  for (const buf of buffers) {
    group.addTask(imageProcessing, { value, buffer: buf, type });
  }
  // 任务组 + HIGH 优先级,系统自动调度
  return mergeArrayBuffers(await taskpool.execute(group, taskpool.Priority.HIGH) as ArrayBuffer[]);
}

对比下来,Worker 代码量明显多,要管线程数量上限、复用、销毁。TaskPool 几行就完事,TaskGroup + 优先级自动调度。

线程创建耗时

Worker 要自己管线程数量、复用、生命周期,避免大量系统资源消耗。TaskPool 系统统一管理,动态调度 + 负载均衡。

结论:创建线程耗时 Worker > TaskPool。应用首帧要快速响应的场景用 TaskPool。

数据传输方式

两者都支持两种传递方式:

  • 转移控制权:transfer 列表中的 ArrayBuffer 转移到工作线程,不复制。传输后宿主线程里这个 ArrayBuffer 失效。
  • 深拷贝:复制一份数据传过去,执行线程修改不影响宿主线程原数据。

底层用同一套序列化/反序列化机制。差异在于 TaskPool 支持任务方法传递,Worker 的任务方法必须写在 Worker.ets 文件里。所以 TaskPool 比 Worker 多了任务方法的序列化/反序列化步骤。

一个任务时 TaskPool 序列化数据(参考):

内容序列化数据量(bytes)序列化时间(μs)反序列化时间(μs)
方法589.54943.749
参数21736.111115.294
结果4716.66785.243

如果用转移控制权,序列化数据量小很多,效率高。深拷贝会增加开销。宿主线程传完数据后不需要紧接着访问的场景,推荐转移控制权。

任务执行耗时

中载和重载环境下,随任务数增多,耗时变化:

重载环境:

  • 任务数 1 时,TaskPool 和 Worker 总耗时相近
  • 任务数 4 时,Worker 效率最高,比单任务减少约 57%
  • TaskPool 在并发数 > 8 后优于 Worker 并趋于稳定,比单任务减少约 50%

中载环境:

  • 任务数 1 时,两者用时相近
  • 任务数 4 时,Worker 略优于 TaskPool
  • 任务数 > 4 时,Worker 稳定在 3.3s 左右
  • TaskPool 在任务数 8 时耗时较多(8 核手机最多 7 个工作线程,第 8 个任务串行)
  • 任务数 > 8 时 TaskPool 稳定在 2.95s 左右
  • 任务数 > 50 时两者差异不大

并发能带来 50%~65% 收益,但不是任务越多越好。重载下 TaskPool 的高优先级任务更容易抢到系统资源,所以 TaskPool 稍快;中载下系统资源充足,高优先级效果不明显,两者差不多。

运行时内存占用

任务数少时两者差别不大。任务数多时 TaskPool 内存比 Worker 大,因为 TaskPool 多了调度器和线程池的封装开销。任务执行完后都会回收释放。

选型决策树

是

否

是

否

是

否

是

否

是

否

新任务

同步执行 > 3 分钟?

用 Worker

需要保存线程上下文
如句柄状态?

需要频繁取消任务?

用 TaskPool

需要优先级 / 任务组 / 延时?

任务量大且内存敏感?

文字版决策路径:

  1. 同步执行超过 3 分钟 → Worker
  2. 需要保存线程上下文(句柄、长期状态) → Worker
  3. 需要频繁取消任务 → TaskPool
  4. 需要优先级 / 任务组 / 延时 / 周期任务 → TaskPool
  5. 任务量大且内存敏感 → Worker
  6. 其他场景 → TaskPool

总结一下下哦

别教条。 决策树是参考,不是教条。有些场景两者都能用,选你写得快、维护成本低的。一个 2 分钟的独立计算任务,用 TaskPool 写 5 行代码搞定,没必要为了"理论最优"硬上 Worker 写 50 行。

TaskPool 的 3 分钟限制只算同步执行。 异步 I/O 等待不计入。数据库异步插入、网络请求的等待时间都不算,只算 CPU 实际处理时间。别看到 3 分钟就慌,先看任务是同步还是异步。

Worker 复用是关键。 频繁创建销毁 Worker 开销大。如果用 Worker,做好复用池,一个 Worker 跑完任务别立刻销毁,看还有没有后续任务。

重载下 TaskPool 优先级才有优势。 中载下系统资源充足,TaskPool 的高优先级效果不明显,和 Worker 差不多。重载下高优先级任务更容易抢资源,TaskPool 才快。

任务数不是越多越好。 8 核手机 TaskPool 最多 7 个工作线程,任务数超过 7 后会有任务串行。根据任务计算量和 CPU 核数调任务数,一般 4~8 个并发收益就到顶了。

数据传输用转移控制权。 大 ArrayBuffer 传输,宿主线程传完不再访问的场景,用 setTransferList() 转移所有权,比深拷贝快很多。

内存占用监控。 TaskPool 任务多时内存占用比 Worker 高,但任务完会回收。如果应用整体内存紧张,且任务量大,考虑 Worker。否则 TaskPool 的便利性更值得。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐