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
詳細
Taskpool の依存関係分析スキル
このスキルは、静的 ArkTS ランタイムにおける Taskpool の依存関係の問題を体系的に診断し、修正するのに役立ちます。ユーザーコードから依存関係グラフを構築し、循環を検出し、不正な API 呼び出し順序を特定し、通知/アクティベーション経路を追跡して、修正を提案する手順を説明します。
このスキルが必要な理由
Taskpool の依存関係は、「単なるデッドロック検出」ではありません。独自の状態マシン、通知メカニズム、タスク種別の制約、キャンセル伝播ルールを備えた、独自のタスク依存関係モデルです。あるタスクが別のタスクに依存している場合、そのタスクは taskDependenciesCount がゼロになるまで waitForDependencies() を介してブロックします。前提タスクが終了すると、notifyDependencies() を呼び出してカウンターを減らし、tryActivatePendingDependencyTask を介して、実行可能になった依存先タスクを再度キューに登録します。この一連の処理のどこかで問題が起きると、依存先タスクは気づかれないまま停止するか、実行されなくなります。根本原因は、多くの場合、症状が現れる場所とは異なるところにあります。
ソースコードの参照先
このスキルを使用する際は、正確な実装を確認するため、以下のソースファイルを参照してください。
-
静的標準ライブラリ:
runtime_core/static_core/plugins/ets/stdlib/std/concurrency/taskpool.etsTask.addDependency(1238 行目付近):タスク種別を検証し、hasTaskDFSで循環依存を確認して、dependentTasksセットにエッジを追加しますTask.removeDependency(1284 行目付近):状態を検証し、エッジを削除して、taskDependenciesCountを調整しますTask.hasTaskDFS(1371 行目付近):dependentTasksをたどる DFS で循環を検出しますInternalTask.waitForDependencies(718 行目付近):taskDependenciesCount == 0になるまでtaskMutex/condVarでブロックしますInternalTask.notifyDependencies(731 行目付近):各依存先のカウンターを減らし、条件変数にシグナルを送り、実行可能なタスクをアクティベートします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の辺を作成します(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、execute 後) | いいえ(10200070) | いいえ(10200079) | いいえ(10200071) |
| 周期タスク | いいえ(10200113) | いいえ(10200113) | いいえ(10200113) |
| グループタスク | いいえ(10200073) | いいえ(10200074) | 該当なし |
| SeqRunner タスク | いいえ(10200070) | いいえ(10200079) | いいえ(10200071) |
| AsyncRunner タスク | いいえ(10200070) | いいえ(10200079) | いいえ(10200071) |
| タイムアウトタスク | いいえ | いいえ | 該当なし |
違反があれば、該当するエラーコードとともに報告します。
3c:依存関係の辺の欠落
removeDependency のエラーについては、削除時点でその辺が実際にグラフ内に存在するか確認します。ユーザーコードで removeDependency(taskX) を呼び出していても、taskX が呼び出し元の前提タスクでない場合は、エラー 10200027 です。
Step 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 がシグナルされない | ワーカーが再エンキューされたタスクを取得しない | エンキューとワーカーのスリープの間で競合が発生した |
「起動しない」問題を診断する場合は、次の箇所にログを追加します。
notifyDependencies:各依存タスクの taskId と、新しいtaskDependenciesCountを記録するtryActivatePendingDependencyTask:優先度が見つかったか、タスクが CANCELED か、再エンキューに成功したかを記録するwaitForDependencies:待機開始時と終了時のログを記録する(最後のtaskDependenciesCountも記録する)moveToPendingDependencyQueue:タスクを保留キューに移したときと、スキップしたとき(依存関係がすでに解消されている場合)を記録する
Step 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 状態のまま停止する:依存タスクがグローバルキューにあり、キャンセル時に削除されなかった(動的では
RemoveDependTaskByTaskId、静的ではtryCancelDependentTasksで処理されるはずです) - 二重キャンセルの競合:同じ依存関係チェーン内の 2 つのタスクが同時にキャンセルされる。状態遷移に対して
cancel()が適切な CAS を行っているか確認する
Step 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に存在してはなりません。 -
タスク型の制限に関する不変条件:
addDependencyに参加できるのは、未実行の TASK 型と FUNCTION_TASK のみです。Periodic、group、seqRunner、asyncRunner、および実行済み(COMMON_TASK)型は、どちらの方向でも禁止されています。 -
通知に関する不変条件:COMMON_TASK が(キャンセルされずに)終了した場合、
notifyDependencies()を呼び出す必要があり、this.dependentTasksのすべてのエントリーを走査する必要があります。エントリーが欠けていると、依存先タスクの一部が起動しないままになります。 -
キャンセル伝播に関する不変条件:タスクがキャンセルされた場合、その
dependentTasks内のすべてのタスクも再帰的にキャンセルする必要があります。依存先タスクがキャンセルされないと、キャンセルされた前提タスクを待ち続けて永久に停止するか、前提タスクの結果が欠けた状態で実行されます。 -
単一インスタンスに関する不変条件:
waitForDependencies()は、依存関係が満たされた後にisRunningを確認します。同じタスクの別のインスタンスが実行中の場合、現在のインスタンスは待機します。notifyDependencies()はisRunning = falseを設定し、condVarにシグナルを送って次のインスタンスのブロックを解除します。 -
保留キューの整合性:タスクは、グローバルキュー →
pendingDependencyTasks(キューから取り出されたもののブロックされている場合)→ グローバルキュー(tryActivatePendingDependencyTaskによって再キュー登録された場合)と移動します。グローバルキューとpendingDependencyTasksの両方に同時に存在してはなりません。execute()の開始時に呼び出されるremovePendingDependencyTaskによって、クリーンアップが行われます。
注意すべきアンチパターン
これらのユーザーコードのパターンは、ほぼ確実にバグです。すぐに指摘してください。
- execute 後の 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 | 実行済みタスクまたはランナータスクに対する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 | 依存関係に周期タスクが含まれています | 依存関係チェーンから周期タスクを削除します |