HarmonyOS TaskPool 并发任务实战:可取消执行、依赖编排与结果回收

TaskPool 适合把 CPU 计算或可拆分任务从主线程移走,但它不是把任意代码丢进去就安全。任务函数的输入输出、取消协作、依赖顺序和异常回收都要提前设计。本文用路线评分批处理场景,把 TaskPool 的可取消任务和依赖编排写成一条可复用链路。

请添加图片描述

本文先把并发失控点讲清

这一节把 TaskPool 当作工程调度能力来看,而不是只看一个 execute 调用。并发任务涉及跨线程数据传输、任务取消、依赖顺序和结果回收,如果这些边界没有提前设计,主线程压力可能降了,异常和状态问题反而会变多。

  • TaskPool 输入先瘦身,只传可序列化计算数据。
  • @Concurrent 函数保持纯计算,并在长循环中响应取消。
  • 调度器持有 Task 引用,页面只通过业务 id 取消。
  • 批量任务带业务 id 回收异常,便于单项重试。

这几件事连在一起看,读者就能从配置、API 调用、状态维护和排查方法四个角度复用本文方案,而不是只复制某一段示例代码。

TaskPool 资料与声明入口

项目 内容
官方能力 TaskPool 支持任务并发执行、取消、依赖和任务组。
本地声明 D:/harmonyos/SDK/23/ets/api/@ohos.taskpool.d.ts
关键 API Task、execute、cancel、Task.isCanceled、addDependency、TaskGroup。
限制 并发函数需要可序列化输入输出,子线程不应直接操作 UI。

并发任务的版本与线程边界

项目 内容
SDK HarmonyOS SDK 23,已核对 Task 构造、execute、cancel 与依赖接口。
示例场景 路线文件解析、评分计算、结果打包。
线程边界 任务函数只做计算和数据整理,UI 更新回到主线程。
风险点 大对象拷贝、取消不协作、依赖顺序不清晰。

请添加图片描述

请添加图片描述

先拆任务,不把页面对象传进去

TaskPool 会跨线程执行,传入页面实例、Context、闭包里的 UI 状态都容易出问题。更稳的做法是只传可序列化数据。

export interface RoutePointLite {
  lat: number;
  lng: number;
  altitude: number;
}

export interface RouteScoreInput {
  routeId: string;
  points: RoutePointLite[];
  userLevel: 'beginner' | 'regular' | 'expert';
}

输入模型是线程边界。它只保留计算需要的字段,不携带页面、服务对象或复杂原型。

并发函数保持纯计算

@Concurrent 函数里不要读写页面状态,也不要依赖外部单例。输入是什么,输出就只由输入决定,排查结果会简单很多。

import taskpool from '@ohos.taskpool';

@Concurrent
export function calculateRouteScore(input: RouteScoreInput): number {
  let climb = 0;
  for (let i = 1; i < input.points.length; i++) {
    if (taskpool.Task.isCanceled()) {
      throw new Error('route score task canceled');
    }
    climb += Math.max(0, input.points[i].altitude - input.points[i - 1].altitude);
  }
  const levelFactor = input.userLevel === 'beginner' ? 1.2 : input.userLevel === 'expert' ? 0.85 : 1;
  return Math.round(climb * levelFactor);
}

任务函数拥有计算逻辑和取消检查。取消不是外部强杀,而是函数在循环里主动响应。

任务对象要由调度器创建

页面只提交业务输入,调度器负责创建 Task、保存任务引用和回收结果。这样取消和状态展示都有统一入口。

export class RouteScoreScheduler {
  private running = new Map<string, taskpool.Task>();

  async score(input: RouteScoreInput): Promise<number> {
    const task = new taskpool.Task(`score-${input.routeId}`, calculateRouteScore, input);
    this.running.set(input.routeId, task);
    try {
      return await taskpool.execute(task, taskpool.Priority.MEDIUM) as number;
    } finally {
      this.running.delete(input.routeId);
    }
  }

  cancel(routeId: string): void {
    const task = this.running.get(routeId);
    if (task) {
      taskpool.cancel(task);
    }
  }
}

调度器负责 Task 生命周期。页面不用保存 Task 引用,只通过 routeId 发起取消。

依赖任务要写出先后关系

先解析文件,再计算评分,最后打包展示结果。依赖关系应该写在任务对象上,而不是靠 await 顺序散落在多个函数里。

@Concurrent
export function parseRoute(raw: string): RoutePointLite[] {
  return JSON.parse(raw) as RoutePointLite[];
}

@Concurrent
export function packScore(routeId: string, score: number): string {
  return JSON.stringify({ routeId, score, generatedAt: Date.now() });
}

export function buildScorePipeline(raw: string, input: RouteScoreInput): taskpool.Task[] {
  const parseTask = new taskpool.Task('parse-route', parseRoute, raw);
  const scoreTask = new taskpool.Task('score-route', calculateRouteScore, input);
  const packTask = new taskpool.Task('pack-score', packScore, input.routeId, 0);
  scoreTask.addDependency(parseTask);
  packTask.addDependency(scoreTask);
  return [parseTask, scoreTask, packTask];
}

这里展示依赖建模方式。真实项目中 packTask 的输入可由上层拿到 score 后再创建,重点是让依赖关系显式可见。

任务组适合批量同类任务

多条路线同时评分时,可以用 TaskGroup 承接一批同类任务。结果回到主线程后再排序和渲染。

export async function scoreRoutesBatch(inputs: RouteScoreInput[]): Promise<number[]> {
  const group = new taskpool.TaskGroup('route-score-batch');
  for (const input of inputs) {
    group.addTask(calculateRouteScore, input);
  }
  const result = await taskpool.execute(group, taskpool.Priority.LOW);
  return result as number[];
}

任务组负责同类批处理。优先级设为 LOW,避免和用户当前交互竞争主资源。

取消要配合 UI 状态

用户点击取消后,页面应该先进入“取消中”,等 Promise 进入 catch 或 finally 后再恢复按钮状态。

export async function runWithCancelState(input: RouteScoreInput, scheduler: RouteScoreScheduler, state: ScorePageState): Promise<void> {
  state.status = 'running';
  try {
    state.score = await scheduler.score(input);
    state.status = 'done';
  } catch (err) {
    state.status = String(err).includes('canceled') ? 'canceled' : 'failed';
  }
}

UI 状态只处理 Promise 结果,不进入 @Concurrent 函数。线程边界清晰后,取消反馈会稳定。

大数据输入先瘦身

如果把完整地图对象、图片缓存或复杂对象传入 TaskPool,序列化成本会抵消并发收益。传入前先压缩为计算需要的字段。

export function toRouteScoreInput(route: RouteEntity): RouteScoreInput {
  return {
    routeId: route.id,
    userLevel: route.level,
    points: route.trackPoints.map(point => ({
      lat: point.lat,
      lng: point.lng,
      altitude: point.altitude
    }))
  };
}

瘦身函数属于主线程准备阶段。它把复杂实体转换成轻量输入,减少跨线程传输成本。

异常要带业务 id 回收

批量任务里只看到一个错误栈,定位不到是哪条数据失败。任务入口和调度器都要带 routeId。

export class RouteTaskError extends Error {
  constructor(readonly routeId: string, message: string) {
    super(`[${routeId}] ${message}`);
  }
}

export function assertRouteInput(input: RouteScoreInput): void {
  if (!input.routeId || input.points.length < 2) {
    throw new RouteTaskError(input.routeId, 'route points are not enough');
  }
}

业务异常携带 routeId。结果回收时可以只重试失败路线,不影响其他已完成任务。

TaskPool 排查表:从取消无效倒查

现象 优先查看 处理方式
取消后任务还在跑 并发函数内部没有调用 Task.isCanceled 在长循环或分段处理处主动检查。
执行后页面卡顿 传入对象太大或结果太大 先瘦身数据,只传计算字段。
子线程访问 UI 报错 @Concurrent 函数里引用了页面状态 并发函数只接收普通数据,UI 更新回主线程。
批量任务定位不到失败项 异常里没有业务 id 自定义错误带 routeId。

并发上线前的核对清单

这份清单建议在提交代码、写入团队文档或交给测试同学前逐项过一遍。它不是形式化备注,而是把本文的配置边界、运行时行为、异常兜底和可观测信息压成可以执行的确认项。

  • @Concurrent 函数不访问 UI、Context 和页面实例。
  • 输入输出是可序列化的轻量对象。
  • 长任务内部有 Task.isCanceled 检查。
  • 调度器保存 Task 引用并在 finally 回收。
  • 批量任务异常能定位到业务 id。

如果其中任意一项还没有办法给出明确证据,优先回到对应实现小节补日志、补校验或补生命周期处理,再进入下一轮联调。

TaskPool 编排小结

TaskPool 的重点是任务边界,而不是单纯把代码放到子线程。输入瘦身、纯计算函数、协作取消、依赖编排和结果回收都设计好后,并发才会真正提高稳定性。

TaskPool 参考资料

下面列出的资料用于核对 API 名称、能力范围和版本边界。实际落地时还需要结合项目使用的 SDK 版本、设备 API 级别以及团队已有封装做一次复核。

  • TaskPool 使用指南:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/task-pool-usage-guidelines
  • HarmonyOS SDK 23 本地 API 声明:@ohos.taskpool.d.ts
Logo

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

更多推荐