HarmonyOS TaskPool 并发任务实战:可取消执行、依赖编排与结果回收
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
更多推荐




所有评论(0)