关于 taskpool-dependency-analysis
taskpool-dependency-analysis 用于诊断静态 ArkTS 运行时中的任务依赖问题,指导开发者构建依赖图、检查循环与 API 调用顺序、追踪通知和取消传播并提出修复建议。
主要功能
- 01根据代码构建任务依赖图
- 02检测循环和非法任务类型边
- 03追踪通知与任务激活路径
- 04分析取消传播
- 05提供结构化诊断与修复建议
适用场景
- 01排查 taskpool 任务未执行的问题
- 02诊断循环或缺失的依赖关系
- 03发现无效的依赖 API 调用顺序
- 04追踪任务间的取消传播故障
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ openharmonyinsight/ openharmony-skills --skill taskpool-dependency-analysis安装后在 Claude Code 中输入/taskpool-dependency-analysis确认已加载。
附带文件evals/evals.jsonreferences/dependency_reference.md
详细介绍
任务池依赖分析技能
此技能可帮助你系统地诊断和修复静态 ArkTS 运行时中的任务池依赖问题。它将引导你从用户代码构建依赖图、检测循环、识别非法 API 调用顺序、追踪通知/激活路径,并提供修复建议。
此技能的用途
任务池依赖并非“只是检测死锁”,而是一套定制的任务依赖模型,具有自身的状态机、通知机制、任务类型限制和取消传播规则。依赖于另一任务的任务会通过 waitForDependencies() 阻塞,直到其 taskDependenciesCount 归零;前置任务完成后,会调用 notifyDependencies(),递减计数器,并通过 tryActivatePendingDependencyTask 将已就绪的依赖任务重新加入队列。如果此链条中的任一步骤出现问题,依赖任务就可能悄无声息地挂起或永不执行,而根本原因通常并非表面症状出现的位置。
源代码绑定
使用此技能时,请阅读以下源文件,以权威实现为准:
-
静态标准库:
runtime_core/static_core/plugins/ets/stdlib/std/concurrency/taskpool.etsTask.addDependency(第 ~1238 行):验证任务类型,通过hasTaskDFS检查循环依赖,并在dependentTasks集合中建立边Task.removeDependency(第 ~1284 行):验证状态、移除边,并调整taskDependenciesCountTask.hasTaskDFS(第 ~1371 行):遍历dependentTasks进行 DFS 循环检测InternalTask.waitForDependencies(第 ~718 行):在taskDependenciesCount == 0前通过taskMutex/condVar阻塞InternalTask.notifyDependencies(第 ~731 行):递减每个依赖任务的计数、发出 condvar 信号,并激活已就绪任务tryActivatePendingDependencyTask(第 ~3014 行):将依赖均已满足的任务重新加入队列moveToPendingDependencyQueue(第 ~2993 行):在任务出队但仍被阻塞时,将任务和优先级存入待处理映射
-
动态参考:
commonlibrary/ets_utils/js_concurrent_module/taskpool/task.cpp和task_manager.cppTask::AddDependency、Task::RemoveDependency、Task::CheckAddDependencyTaskManager::StoreTaskDependency、TaskManager::CheckCircularDependency、TaskManager::NotifyDependencyTaskInfo、TaskManager::RemoveTaskDependency、TaskManager::ClearDependentTask
-
测试参考:
runtime_core/static_core/plugins/ets/tests/ets-common-tests/taskpool/common_tasks.etsdependentTasksTest、circularDependencyTest、dependencyExecutedTaskTest、dependencyCancelTaskTest、doubleCancelByDependencyTest
有关数据结构、错误码和完整生命周期的详细参考,请阅读此技能目录中的 references/dependency_reference.md。
分析工作流
请按顺序执行以下步骤。不要跳过或合并步骤。
步骤 1:对问题进行分类
阅读用户的描述或失败的测试用例,并将问题归入以下某一类别:
| 类别 | 常见症状 | 错误码 |
|---|---|---|
| 循环依赖 | addDependency 立即抛出异常 | 10200026 |
| 缺少/不存在的依赖项 | removeDependency 抛出异常 | 10200027、10200097 |
| 依赖任务始终无法唤醒 | 已提交任务但始终不执行;无声挂起 | 无(不会抛出错误) |
| 取消传播失败 | 取消前置任务不会取消依赖任务,反之亦然 | 无 |
| 非法 API 调用顺序 | 对错误类型的任务调用 addDependency/removeDependency | 10200070、10200071、10200073、10200074、10200079、10200113 |
| 依赖后重新执行 | 重新执行已经带依赖运行过的任务 | 10200072 |
| 超时与依赖冲突 | 对带有依赖项的任务调用 execute(task, {timeout}) | 自定义错误 |
如果用户提供了测试用例(.ets 文件),请先阅读,并提取:任务创建顺序、addDependency 调用、execute 调用,以及预期行为与实际行为。
步骤 2:根据用户代码构建依赖图
从用户代码或测试用例中,按顺序提取所有 Task 创建、addDependency、removeDependency 和 execute 调用。构建有向图:
- 节点:每个
Task实例(如果有,使用变量名 + taskId 标记) - 边:每个
addDependency都会创建一条边dependent → prerequisite(依赖任务会等待前置任务) - 边删除:每个
removeDependency都会移除对应的边 - 执行状态:根据
execute/cancel调用,将节点标记为unexecuted、submitted、running、finished、canceled
以文本形式表示图,例如:
task1 ──depends on──> task2 ──depends on──> task3
task4 ──depends on──> task2
如果通过 removeDependency 修改了图,请展示每个重要状态变化时的图。
步骤 3:检测循环和非法边
在图上执行以下检查:
3a:循环检测
从每个节点开始使用 DFS。如果某条路径返回起始节点,则存在循环。报告确切的循环路径:
Cycle detected: task1 → task2 → task3 → task1
Introduced by: task3.addDependency(task1) at line X
静态实现使用 hasTaskDFS 遍历 dependentTasks(反向遍历:从前置任务指向其依赖任务)。确保循环检测遵循此遍历方向。
3b:非法任务类型的边
根据以下规则检查每个节点(这些是 addDependency 和 removeDependency 中的确切检查条件):
| 任务类型 | 能否调用 addDependency? | 能否作为依赖目标? | 能否调用 removeDependency? |
|---|---|---|---|
| 未执行(TASK) | 是 | 是 | 是(如果 isDependent) |
| 已执行(COMMON_TASK,执行后) | 否(10200070) | 否(10200079) | 否(10200071) |
| 周期性任务 | 否(10200113) | 否(10200113) | 否(10200113) |
| 组任务 | 否(10200073) | 否(10200074) | 不适用 |
| SeqRunner 任务 | 否(10200070) | 否(10200079) | 否(10200071) |
| AsyncRunner 任务 | 否(10200070) | 否(10200079) | 否(10200071) |
| 超时任务 | 否 | 否 | 不适用 |
报告所有违规,并注明具体错误码。
3c:缺少依赖边
对于 removeDependency 错误:确认移除时该边确实存在于图中。如果用户代码调用 removeDependency(taskX),但 taskX 不是调用方的前置任务,则会产生错误 10200027。
步骤 4:追踪通知/激活路径
对于“依赖任务始终无法唤醒”的问题,这是最重要的一步。从前置任务完成时开始,追踪完整路径,直到依赖任务应当执行为止:
Prerequisite finishes
→ InternalTask.execute() finally block
→ notifyDependencies() called
→ Iterates over prerequisite.dependentTasks
→ For each dependent:
- Locks dependent.taskMutex
- Decrements dependent.taskDependenciesCount
- Signals dependent.condVar
- If taskDependenciesCount == 0:
→ tryActivatePendingDependencyTask(dependent)
- Takes priority from pendingDependencyTasks map
- Re-enqueues dependent into globalTaskQueue
- Signals global condvar to wake a worker
检查每个步骤是否可能失败:
| 失败点 | 症状 | 根本原因模式 |
|---|---|---|
未调用 notifyDependencies | 依赖任务始终无法唤醒 | 任务类型不是 COMMON_TASK(动态模式:TriggerTask 只会为普通任务调用 NotifyDependencyTaskInfo);或者任务已取消(isCancel=true 会跳过通知) |
dependentTasks 集合为空 | 未通知任何依赖任务 | 边在执行前已被移除,或者在 execute 之后才调用 addDependency |
taskDependenciesCount 未降至 0 | 依赖任务被唤醒后又重新等待 | 某个前置任务完成时没有递减该依赖任务的计数(竞态:通知运行后才递增计数) |
pendingDependencyTasks 中没有该依赖任务的条目 | tryActivatePendingDependencyTask 返回 false | 任务从未移入待处理队列(例如,任务已出队,并在所有前置任务完成之前开始 waitForDependencies) |
| 任务在 tryActivatePendingDependencyTask 中处于 CANCELED 状态 | Promise 因取消错误而被拒绝 | 另一个线程/任务在通知与激活之间取消了该依赖任务 |
| 全局 condvar 未发出信号 | Worker 不会提取重新入队的任务 | 入队与 Worker 休眠之间发生竞态 |
诊断“始终无法唤醒”的问题时,请在以下位置添加日志:
notifyDependencies:记录每个依赖任务的 taskId 和新的taskDependenciesCounttryActivatePendingDependencyTask:记录是否找到优先级、任务是否处于 CANCELED 状态,以及重新入队是否成功waitForDependencies:记录开始等待和结束等待时的情况(包括最终的taskDependenciesCount)moveToPendingDependencyQueue:记录任务移入待处理队列的情况,以及跳过任务的情况(依赖关系已清除)
步骤 5:追踪取消传播
任务被取消时,其依赖任务也应当被取消。追踪这条路径:
cancel(task) called
→ task state -> CANCELED (CAS)
→ tryCancelDependentTasks(task)
→ Iterates over InternalTask.of(task).dependentTasks
→ For each dependent:
- calls cancel(dependent) (silently catches errors)
→ cleanupCanceledPendingDependencyTask(task)
→ Takes priority from pendingDependencyTasks
→ If found: finishCanceledPendingDependencyTask (rejects promises)
检查以下情况:
- 依赖任务未被取消:
dependentTasks集合为空,或者边已被移除 - 依赖任务卡在 WAITING 状态:它们位于全局队列中,而 cancel 未将其移除(动态模式下应由
RemoveDependTaskByTaskId处理,静态模式下应由tryCancelDependentTasks处理) - 重复取消竞态:同一依赖链中的两个任务被并发取消——检查
cancel()是否在状态转换时正确使用 CAS
步骤 6:生成诊断报告
输出一份包含以下部分的结构化报告:
依赖图
Nodes: [task1 (unexecuted), task2 (finished), task3 (submitted, isDependent=true)]
Edges:
task3 → task2 (task3 depends on task2)
task1 → task3 (task1 depends on task3)
发现的问题
对于每个问题,请提供:
- 问题类型:循环/非法边/缺失边/通知失败/取消失败
- 错误代码(如适用)
- 准确的源代码位置(文件:行)
- 受影响的边或路径
- 根本原因说明
建议的修复方案
对于每个问题,建议采用以下其中一种方案:
- 调整 addDependency 调用顺序:在 execute 之前声明所有依赖关系
- 移除问题边:在任务类型更改之前使用 removeDependency
- 拆分任务:创建一个尚未执行的新任务,而不是重复使用已执行的任务
- 替换任务类型:需要依赖关系时,不要使用 periodic/group/seqRunner/asyncRunner
- 添加同步机制:如果问题是通知/激活竞态,请指出具体的竞态时间窗口及修复方案(不要建议使用 sleep/retry/timeout)
- 修复通知路径:如果未调用 notifyDependencies,请指出原因并修复相关条件
重构代码示例
如果用户的代码存在结构性问题(例如,在 execute 之后添加依赖关系、创建自引用循环,或在依赖链中使用组任务),请提供一段修正后的代码示例,展示正确的操作顺序。
关键不变量检查清单
分析任何任务池依赖问题时,请确认以下不变量均成立。如果有任何一项被违反,它就是根本原因或促成因素:
-
边方向不变量:
addDependency(taskA, taskB)表示 taskA 等待 taskB。边存储在taskB.dependentTasks中(反向映射)。如果遍历dependentTasks,遍历方向是从前置任务到依赖任务,而不是从依赖任务到前置任务。 -
taskDependenciesCount 不变量:对于任意任务 T,
T.taskDependenciesCount必须等于指向 T 的前置任务边数。如果taskDependenciesCount > 0,T 必须位于pendingDependencyTasks中(如果已从全局队列出队),或者位于全局队列中(如果刚提交)。如果taskDependenciesCount == 0,T 不得位于pendingDependencyTasks中。 -
任务类型限制不变量:只有尚未执行的 TASK 类型和 FUNCTION_TASK 才能参与
addDependency。禁止周期任务、组任务、seqRunner、asyncRunner 以及已执行的(COMMON_TASK)类型参与依赖关系的任一端。 -
通知不变量:COMMON_TASK 完成时(未取消),必须调用
notifyDependencies(),且必须遍历this.dependentTasks中的所有条目。若有任何条目缺失,某个依赖任务将永远无法唤醒。 -
取消传播不变量:任务取消时,其
dependentTasks中的所有任务也必须递归取消。若有任何依赖任务未取消,它将一直挂起(等待已取消的前置任务),或在缺少前置任务结果的情况下执行。 -
单实例不变量:满足依赖条件后,
waitForDependencies()会检查isRunning。如果同一任务的另一个实例正在运行,当前实例会等待。notifyDependencies()会设置isRunning = false并通知condVar,以解除下一个实例的阻塞。 -
待处理队列一致性:任务会从全局队列移动到
pendingDependencyTasks(出队但受阻时),再由tryActivatePendingDependencyTask重新加入全局队列。任务不得同时位于全局队列和pendingDependencyTasks中。execute()开始时调用removePendingDependencyTask可确保完成清理。
需要留意的反模式
用户代码中出现以下模式时,几乎总是存在错误。请立即指出:
- 执行后调用 addDependency:在
taskpool.execute(task)之后调用task.addDependency(other)——抛出 10200070 - 自依赖:
task.addDependency(task)——抛出 10200026 - 依赖链中包含周期任务:
periodicTask.addDependency(x)或x.addDependency(periodicTask)——抛出 10200113 - 依赖链中包含组任务:组任务不能添加或接收依赖——抛出 10200073/10200074
- 缺少前置任务通知:如果通过 seqRunner/asyncRunner/group 执行前置任务,它将永远不会调用
notifyDependencies,因此依赖任务会一直挂起 - 依赖完成后重新执行:任务已带依赖运行后,再调用
taskpool.execute(task)——抛出 10200072 - 对错误的任务调用 removeDependency:移除不存在的依赖——抛出 10200027
- 取消时未传播:取消前置任务,却期望依赖任务仍能运行——依赖任务也会被取消
错误代码速查
| 代码 | 常量 | 含义 | 常见修复方法 |
|---|---|---|---|
| 10200026 | CIRCULAR_DEPENDENCY | 依赖图中存在循环 | 移除导致循环闭合的边 |
| 10200027 | DEPENDENCY_NOT_EXIST | 对非边调用 removeDependency | 仅移除实际添加过的边 |
| 10200070 | EXECUTED_TASK_ADD_DEPENDENCY | 对已执行的任务或 runner 任务调用 addDependency | 在执行前声明依赖 |
| 10200071 | EXECUTED_TASK_REMOVE_DEPENDENCY | 对已执行的任务调用 removeDependency | 执行后不要修改依赖 |
| 10200073 | GROUP_TASK_ADD_DEPENDENCY | 组任务调用 addDependency | 将组任务从依赖链中移除 |
| 10200074 | GROUP_TASK_RELIED_ON | 组任务作为依赖目标 | 使用常规任务,而非组任务 |
| 10200079 | SEQ/ASYNC_RUNNER_TASK_RELIED_ON | SeqRunner/AsyncRunner 作为依赖目标 | 改用常规任务 |
| 10200097 | NO_DEPENDENCY | 对没有依赖项的任务调用 removeDependency | 确保先调用 addDependency |
| 10200101 | NO_PARAMS | 不带参数调用 addDependency/removeDependency | 提供任务参数 |
| 10200113 | PERIODIC_TASK_ADD_DEPENDENCY | 依赖项中包含周期性任务 | 将周期性任务从依赖链中移除 |