oh-arkruntime-thread-safety-audit について
ArkCompiler Runtime Core のスレッド安全性をレビュー、修正するスキルです。ETS コードを中心に、根拠に基づくリスク評価、並行経路の分析、同期方法の選択、テストを支援します。
主な機能
- 01ETS の共有可変状態を検索
- 02並行エントリーポイントを追跡
- 03影響と根拠に基づくリスク分類
- 04状態の不変条件に応じた同期方法の選択
- 05修正と並行テストのレビュー
活用シーン
- 01ETS 標準ライブラリ変更のデータ競合監査
- 02ETS API に関する TSAN レポートの調査
- 03シングルトン、キャッシュ、カウンターの同期レビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ openharmonyinsight/ openharmony-skills --skill oh-arkruntime-thread-safety-auditインストール後、Claude Code で/oh-arkruntime-thread-safety-auditを入力して読み込まれたか確認します。
詳細
OH Ark Runtime のスレッドセーフティ監査
このスキルを使って、ArkCompiler Runtime Core のスレッドセーフティに関する問題を調査、レビュー、修正します。特に、次の領域が対象です。
static_core/plugins/ets/stdlib/**/*.ets配下の ETS 標準ライブラリファイルstatic_core/plugins/ets/**配下の ETS ランタイム/プラグインサポートコードstatic_core/plugins/ets/tests/配下のテスト- ETS API がネイティブ境界を越える場合、または TSAN レポートで示されている場合に限り、関連するネイティブ C++
よくあるきっかけは、ArkTS-Sta の並行実行モデルです。標準ライブラリの公開 API は taskpool、EAWorker、複数のコルーチン/ワーカーから呼び出される可能性があるため、静的な可変状態とグローバルシングルトンは、スレッド間で共有されると証明できない限り、共有されるものとして扱う必要があります。
ソースで確認できる API
このチェックアウトまたは OpenHarmony の公式ドキュメントで確認できる API/型のみを使用してください。以下の例は、このリポジトリのソースで確認できます。エクスポートされていないヘルパーは、公開 API の推奨ではなく、内部実装の参照として扱ってください。
| API/型 | ソース |
|---|---|
AtomicInt、AtomicLong、AtomicBoolean | static_core/plugins/ets/stdlib/std/concurrency/Atomics.ets:83、static_core/plugins/ets/stdlib/std/concurrency/Atomics.ets:194、static_core/plugins/ets/stdlib/std/concurrency/Atomics.ets:782 |
Atomics.load | static_core/plugins/ets/stdlib/std/concurrency/LegacyAtomics.ets:253 |
ConcurrentHashMap | static_core/plugins/ets/stdlib/std/containers/ConcurrentHashMap.ets:131 |
Mutex、QueueSpinlock | static_core/plugins/ets/stdlib/std/core/SyncPrimitives.ets:43、static_core/plugins/ets/stdlib/std/core/SyncPrimitives.ets:156 |
ConcurrencyHelpers.mutexCreate、ConcurrencyHelpers.lockGuard | static_core/plugins/ets/stdlib/std/core/ConcurrencyHelpers.ets:27、static_core/plugins/ets/stdlib/std/core/ConcurrencyHelpers.ets:31。std/concurrency と std/containers にも、パッケージローカルのバリアントがあります。 |
taskpool.execute | static_core/plugins/ets/stdlib/std/concurrency/taskpool.ets:1841、static_core/plugins/ets/stdlib/std/concurrency/taskpool.ets:1852、static_core/plugins/ets/stdlib/std/concurrency/taskpool.ets:1863 |
EAWorker | static_core/plugins/ets/stdlib/std/core/EAWorker.ets:65 |
CoroutineExtras.stopTaskpool | static_core/plugins/ets/stdlib/std/debug/concurrency/CoroutineExtras.ets:42 |
提案する例で使っている API/型が対象のチェックアウト内で rg により見つからない場合は、その例を削除するか、内部向けの疑似コードとして明記してください。
スキャンのワークフロー
ユーザーが指定したファイル、差分、API サーフェスから始めます。対象ファイルまたはその最も近い所有ディレクトリには、以下の <target-paths> を使ってください。依頼の対象がより狭く指定されていない場合に限り、static_core/plugins/ets 全体に範囲を広げてください。
フェーズ 1:ETS の静的可変状態の検出
ETS/TS ファイル内の静的フィールド、グローバルに見える可変オブジェクト、キャッシュ、ロック、アトミックを探します。
rg -n "static .*[:=]|private static|public static|const .*[:=]|let .*[:=]|new Map|new Array|StringBuilder|ConcurrentHashMap|Mutex|QueueSpinlock|Atomic" \
--type-add 'ets:*.ets' --type-add 'ets:*.ts' -t ets \
<target-paths>
フェーズ 2:並行実行の入口を分析
候補となる状態に、並行実行される経路からアクセスできるかを判断します。
- 公開 API とエクスポートされた関数
taskpool.execute/EAWorker- ランタイムまたは標準ライブラリのコールバック
- native として宣言された ETS メソッド
候補 API の呼び出し箇所をローカルで検索します。
rg -n "taskpool\\.execute|EAWorker|callback|setTimeout|setInterval|native .*\\(|public .*\\(" \
--type-add 'ets:*.ets' --type-add 'ets:*.ts' -t ets \
<target-paths>
登録関数や初期化関数は、それ自体では並行実行の入口ではありません。ワーカーまたはユーザーコードの実行開始後に、それらが並行して呼び出される可能性を示す根拠がある場合に限り、リスクとして扱ってください。
ネイティブコードの追加確認(任意)
ETS の候補がネイティブメソッドの場合、スタックトレースが C++ を指している場合、またはユーザーがネイティブコード/TSAN の分析を明示的に求めた場合に限り、ネイティブコードを確認します。
rg -n "static .*|std::once_flag|std::mutex|os::memory::Mutex|std::atomic|thread_local|std::vector|std::map|std::set|ani_native_function" \
--type cpp \
<related-native-paths>
これらのフェーズの後、修正案を提示する前に各候補を分類します。
| カテゴリ | 通常は安全か? | 確認事項 |
|---|---|---|
static readonly の不変定数 | はい | 内部に可変オブジェクトが隠れていないこと |
| static のプリミティブ型カウンター/フラグ | いいえ | ++、直接の読み書き、可視性 |
static の Map/Array/StringBuilder | いいえ | 並行変更、確認してから実行する処理 |
| シングルトンインスタンス | いいえ | 遅延初期化時の競合 |
| レジストリ/キャッシュ | いいえ | 取得してから作成する処理、古い対応関係、無効化 |
| オブジェクトごとのオブジェクトにあるインスタンスフィールド | 場合による | オブジェクトがグローバルに共有される可能性 |
ConcurrentHashMap/Atomic/Mutex で保護された状態 | 場合による | クリティカルセクションが不変条件全体を保護していること |
どこかにロックがあるという理由だけで、ファイルを安全と判断しないでください。ロックが不変条件全体を保護していることを確認してください。
リスクの基準
次の4つの条件を使って、意味のあるリスクを特定します。
- 共有された可変状態:static フィールド、シングルトンのフィールド、グローバルキャッシュ、またはシングルトンから到達可能な共有オブジェクト。
- 同時実行経路:ユーザーコード、標準ライブラリのコールバック、taskpool、EAWorker、タイマー、またはワーカースレッドから API を呼び出せること。
- 同期の欠如または不備:アトミック処理/ロック/並行コンテナーがない、または確認してから実行する処理の一部しかロックで保護されていないこと。
- 具体的な影響:更新の消失、古い状態、誤った表示結果、タスクのリーク、コールバックの重複、コンテナーの破損など、起こり得る問題を説明できること。
スキャンで static、ConcurrentHashMap、Map、または遅延初期化のパターンが見つかっただけで、High と判定しないでください。まず、並行実行経路と、ユーザーまたはランタイムから見える影響を証明してください。具体的な障害モードを説明できない場合は、コードを変更せずにおくことを優先してください。
リスクレベル:
- High:公開されている並行 API で、クラッシュ、デッドロック、メモリ/オブジェクト/コンテナーの破損、制限のないリソースリーク、または決定的な誤結果が証明されている状態。
- Medium:到達可能な並行実行経路で、更新の消失やライフサイクルの破綻が証明されている状態。たとえば、タスクメタデータのリーク、実行の重複、出力の破損、不適切なクリーンアップなど。
- Low:古いキャッシュ、ID/名前の重複、不正確な診断、統計の欠落、デバッグ/イントロスペクションのみの不整合、まれなタイミングウィンドウなど、影響が限定的または回復可能な競合状態。
- Candidate:到達可能性を示す根拠が弱い、または影響が不明瞭な疑わしいパターン。追加の根拠がない限り、コード変更を推奨しないでください。
- No risk:不変定数、ローカル変数、呼び出しごとのデータ、単一スレッドでの初期化/登録、または所有権が明確で完全に同期された状態。
判断に迷う場合は、重大度を引き上げるのではなく、問題を Candidate として報告してください。Candidate および No risk の検出事項については、コードを変更しないことを優先してください。
よくある不適切なパターン
このセクションのコード例は、ソースコードに基づいたパターンであり、単独で実行するテストではありません。コードにコピーする前に、インポート、パッケージの可視性、および対象ファイルでヘルパーが公開されているか標準ライブラリ内部のものかを確認してください。
遅延シングルトン初期化
不適切な例:
if (Console.instance == undefined) {
Console.instance = new Console()
}
return Console.instance!
修正例:
Console.instanceLock.guard(() => {
if (Console.instance == undefined) {
Console.instance = new Console()
}
})
return Console.instance!
確認と代入は、同じクリティカルセクション内で行う必要があります。
共有出力バッファー
API が文字列を構築してから出力する場合は、処理全体を保護します。
this.outputLock.guard(() => {
let buf = this.lvl2Buf.get(level)!
buf.append(s)
this.printString(buf.toString(), level.valueOf())
this.lvl2Buf.set(level, new StringBuilder)
})
append だけ、または printString だけをロックしても、バッファーが混在する可能性があります。
Shared Map の取得または作成
悪い例:
let slot = map.get(key)
if (slot == undefined) {
slot = new Slot()
map.set(key, slot)
}
return slot
修正パターン:
const existing = states.get(key)
if (existing !== undefined) {
return existing
}
slotLock.lockGuard(() => {
if (states.get(key) === undefined) {
states.set(key, new Slot())
}
})
const slot = states.get(key)
if (slot === undefined) {
throw new Error(`Failed to create slot for '${key}'`)
}
return slot
並行ストレージには ConcurrentHashMap を使用します。アトミックな computeIfAbsent がない場合は、遅延作成のために別のロックを使用します。
同期されていないカウンター
悪い例:
private static uniqueNameCounter: long = -1
return (++Proxy.uniqueNameCounter).toString()
修正:
private static uniqueNameCounter: AtomicLong = new AtomicLong(0)
return Proxy.uniqueNameCounter.fetchAdd(1).toString()
0 からの後置インクリメントの場合:
oldCounter++
アトミックな同等処理も次のとおりです。
counter.fetchAdd(1)
初期値は 0 です。
同期されていないブール値フラグ
悪い例:
private static LOG_ENABLED: boolean = false
Logger.LOG_ENABLED = true
if (Logger.LOG_ENABLED) { ... }
修正:
private static LOG_ENABLED: AtomicBoolean = new AtomicBoolean(false)
Logger.LOG_ENABLED.store(true)
if (Logger.LOG_ENABLED.load()) { ... }
関連するキャッシュフィールド
悪い例:
private static lastParsedTag: string | undefined = undefined
private static lastParsedResult: ParsedLocaleData | undefined = undefined
tag と result の一致が必須なら、それらは一つの不変条件です。読み取りと書き込みを一つのロックで保護するか、一つの不変なキャッシュエントリーオブジェクトに置き換えてください。
最小限の修正:
private static localeCacheMutex: Object = ConcurrencyHelpers.mutexCreate()
private static getCachedLocale(tag: string): ParsedLocaleData | undefined {
let result: ParsedLocaleData | undefined = undefined
ConcurrencyHelpers.lockGuard(Locale.localeCacheMutex, () => {
if (Locale.lastParsedTag == tag && Locale.lastParsedResult != undefined) {
result = Locale.lastParsedResult
}
})
return result
}
private static setCachedLocale(tag: string, data: ParsedLocaleData): void {
ConcurrencyHelpers.lockGuard(Locale.localeCacheMutex, () => {
Locale.lastParsedTag = tag
Locale.lastParsedResult = data
})
}
一方のフィールドだけをロックしないでください。ロックの外で tag を読み取り、ロックの内側で result を読み取ることも避けてください。
スロット内の複合状態
複数のフィールドをまとめて変更する必要がある値には、内部ロックを使用します。
final class TimerSlot {
public tryStart(now: long): boolean {
let started = false
this.lock.guard(() => {
if (!this.active) {
this.startTime = now
this.active = true
started = true
}
})
return started
}
private startTime: long = 0
private active: boolean = false
private lock: QueueSpinlock = new QueueSpinlock()
}
状態マシンを意図的に設計してレビューした場合を除き、複数フィールドの不変条件に個別のアトミック操作を使用しないでください。
修正方法の選択ガイド
実際の不変条件を保護できる、最も限定的な同期方法を選んでください。
| 問題 | 推奨される修正 |
|---|---|
| 単一の数値カウンター | AtomicInt / AtomicLong |
| 単一のブール値フラグ | AtomicBoolean |
| 一致が必要な2つまたは3つのフィールド | すべての読み取り/書き込みを一つのミューテックスで保護 |
| シングルトンの遅延初期化 | ロックで保護された確認後の処理 |
| 共有キー値レジストリ | ConcurrentHashMap + ロック付きの取得または作成 |
| キーごとの可変な複合状態 | スロットごとのロック |
| グローバルな出力行の構築 | 構築から出力までを一つの出力ロックで保護 |
次のことは避けてください。
- 書き込みだけをロックし、読み取りはロックしない
- 一貫性を保つ設計なしに、関連するフィールドへアトミック操作を追加する
- ロックを作成しても、確認後の処理をロックの外に置く
- 外部から見える従来の実行順序を保たずに初期値を変更する
レビューチェックリスト
パッチをレビューする際は、次の点を確認してください。
- 古い競合が実際に解消されていること。再現しにくくなっただけでは不十分です。
- 公開される動作が維持されていること。
- 前置インクリメントの
++xとfetchAdd(1)では、初期値の調整が必要です。 - 後置インクリメントの
x++は、自然にfetchAdd(1)に対応します。
- 前置インクリメントの
- クリティカルセクションが不変条件全体を保護していること。
- 意図した場合を除き、任意のユーザーコールバックを呼び出す間、ロックを保持しないこと。
- 複数のロックを取得する場合、ロックの取得順序に一貫性があること。
ConcurrentHashMapの値に、同期されていない可変フィールドが含まれていないこと。- 未使用のフィールドは削除するか、使用しない理由を記載すること。未使用のフィールドを、実際の競合を修正したものとして扱わないでください。
テスト戦略
リスクに応じたテストを実施してください。
機能的な並行性テスト
新たな API の形式を考案するのではなく、既存のリポジトリテストをひな形として使用してください。
- 関数引数を使った
taskpool.execute:static_core/plugins/ets/tests/ets_func_tests/std/containers/BlockingQueue/AddAndPollStress.ets:47 - テストでの taskpool のクリーンアップ:
static_core/plugins/ets/tests/ets-common-tests/taskpool/common_tasks_new.ets:423 CoroutineExtras.stopTaskpoolの宣言:static_core/plugins/ets/stdlib/std/debug/concurrency/CoroutineExtras.ets:42- 型付き配列のバリア用
Atomics.loadの宣言:static_core/plugins/ets/stdlib/std/concurrency/LegacyAtomics.ets:253
既存のテストで使用されている、単純なワーカータスクのシグネチャを優先してください。開始バリアが必要な場合は、バリアで使用するアトミック操作のソースを示し、スニペットはテストファイル内に記述してください。
TSAN テスト
TSAN が検証の一部である場合は、注記を簡潔にします。競合している変数または関数と、その競合が ETS コード内かネイティブコード内かを記載してください。
対象を絞ったテスト
影響を受ける共有状態に対して、焦点を絞ったテストを追加します。
- 出力 API:行とバッファーの内容が混在しないことを確認します。API が保証していない限り、スレッド間の順序が決定的であることは要求しません。
- 状態を保持するカウンター/タイマー:並行して多数の呼び出しを実行し、合計値/状態遷移を検証します。
- キャッシュ:同一キーでのヒット、異なるキーによる置き換え、オプション/オーバーライド、無効な入力の経路をテストします。
キャッシュに対する並行ミスでは、キャッシュ済みの経路ではなく、より遅い解析経路に入る場合があります。選択したテスト設計に影響する場合に限り、この点に言及してください。
検証コマンド
利用可能な場合は、リポジトリのローカルツールを使用します。
git diff --check -- <changed-files>
ninja -C static_core/build plugins/ets/etsstdlib.abc
static_core/build/bin/es2panda --extension=ets --stdlib=static_core/build/plugins/ets/etsstdlib.abc --output=/tmp/test.abc <test.ets>
static_core/build/bin/ark --boot-panda-files=static_core/build/plugins/ets/etsstdlib.abc --load-runtimes=ets --verification-mode=ahead-of-time /tmp/test.abc <entrypoint>
エントリーポイントは、次の方法で見つけられます。
static_core/build/bin/ark_disasm /tmp/test.abc /tmp/test.pa
rg -n "\\.function .* main\\(" /tmp/test.pa