concurrency-debugging について
C++ と Rust の並行プログラムを調査するエージェント向けデバッグスキルです。競合レポート、ロック順序、デッドロック、atomic の使い方、happens-before の考え方を扱います。
主な機能
- 01ThreadSanitizer の競合レポートの解読
- 02Helgrind によるロック順序分析
- 03GDB によるスレッド検査
- 04よくある std::atomic の誤用の特定
- 05happens-before 関係の推論
- 06Rust の並行処理保証の確認
活用シーン
- 01データ競合の原因となるアクセスの特定
- 02ロック順序によるデッドロックの調査
- 03停止したプロセスのスレッド状態の確認
- 04同期方法とメモリ順序の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mohitmishra786/ low-level-dev-skills --skill concurrency-debuggingインストール後、Claude Code で/concurrency-debuggingを入力して読み込まれたか確認します。
詳細
並行処理のデバッグ
目的
エージェントが並行処理のバグを診断して修正する方法を説明します。ThreadSanitizer のデータ競合レポートの読み方、ロック順序の分析に Helgrind を使用する方法、GDB のスレッド検査によるデッドロックの検出、よくある std::atomic の誤用パターンの特定、C++ と Rust における happens-before の推論を扱います。
トリガー
- 「ThreadSanitizer がデータ競合を報告しました。レポートの読み方を教えてください」
- 「プログラムがデッドロックします。どうデバッグすればよいですか?」
- 「スレッド関連のバグを見つけるために Helgrind を使う方法を教えてください」
- 「std::atomic を正しく使えていますか?」
- 「C++ のメモリ順序において happens-before はどのように機能しますか?」
- 「GDB でデッドロックしているスレッドを特定するにはどうすればよいですか?」
ワークフロー
1. ThreadSanitizer(TSan)— データ競合の検出
# Build with TSan
clang -fsanitize=thread -g -O1 -o prog main.c
# or GCC
gcc -fsanitize=thread -g -O1 -o prog main.c
# Run (TSan intercepts memory accesses at runtime)
./prog
# TSan-specific options
TSAN_OPTIONS="halt_on_error=1:second_deadlock_stack=1" ./prog
TSan レポートの読み方:
WARNING: ThreadSanitizer: data race (pid=12345)
Write of size 4 at 0x7f1234 by thread T2:
#0 increment /src/counter.c:8:5 ← access site in T2
#1 worker_thread /src/counter.c:22:3
Previous read of size 4 at 0x7f1234 by thread T1:
#0 read_counter /src/counter.c:3:14 ← conflicting access in T1
#1 main /src/counter.c:30:5
Thread T2 created at:
#0 pthread_create .../tsan_interceptors.cpp
#1 main /src/counter.c:28:3
SUMMARY: ThreadSanitizer: data race /src/counter.c:8:5 in increment
読み方:
- 1 行目:アクセスの種類(書き込み/読み取り)とアドレス
- 「Write of size」の下のスタック:書き込みを実行したスレッド
- 「Previous read/write」の下のスタック:競合するスレッド
- 「Thread T2 created at」:スレッドが生成された場所
- 修正方法:
increment関数とread_counter関数が、同期せずに同じアドレスにアクセスしています
よくあるデータ競合と修正方法:
| データ競合のパターン | 修正方法 |
|---|---|
| グローバル変数をロックせずに読み取り/書き込みする | ミューテックスを追加するか、std::atomic を使用する |
atomic を使わないダブルチェックロッキング | std::once_flag + std::call_once を使用する |
共有整数に対する += | std::atomic<int>::fetch_add() を使用する |
| 反復処理中にコンテナーが変更される | クリティカルセクション全体をロックする |
shared_ptr の参照カウントに関する競合 | 参照カウントはアトミックなので安全です。ただし、指し示すオブジェクトは安全とは限りません |
2. Helgrind — ロック順序とデータ競合の検出
Helgrind は Valgrind の基盤を利用して、ロック順序違反(デッドロックの可能性)とデータ競合を検出します。
# Run with Helgrind
valgrind --tool=helgrind --log-file=helgrind.log ./prog
# Lock order violation report
==1234== Thread #3: lock order "0x... M2" after "0x... M1"
==1234== observed (incorrect) order
==1234== at pthread_mutex_lock (helgrind/...)
==1234== by worker2 /src/worker.c:45 ← T3 takes M2 then M1
==1234==
==1234== required order established by acquisition of lock at address 0x... M1
==1234== at pthread_mutex_lock
==1234== by worker1 /src/worker.c:31 ← T1 takes M1 then M2
ロック順序違反 = デッドロックの可能性:
- スレッド T1 が M1 を取得し、その後 M2 の取得を試みる
- スレッド T2 が M2 を取得し、その後 M1 の取得を試みる
- 競合すると、両方がデッドロックする可能性がある
修正方法:一貫したグローバルなロック順序を徹底します。常にどこでも M1 を M2 より先に取得します。
3. GDB によるデッドロックの検出
# Attach GDB to a deadlocked process
gdb -p $(pgrep prog)
# Or run under GDB then trigger deadlock
(gdb) info threads # list all threads and current state
# * 1 Thread 0x... (LWP 1234) "prog" ... in __lll_lock_wait ()
# 2 Thread 0x... (LWP 1235) "prog" ... in __lll_lock_wait ()
# Threads blocked in __lll_lock_wait = waiting for mutex
(gdb) thread 1
(gdb) bt # show which mutex thread 1 is waiting for
(gdb) thread 2
(gdb) bt # show which mutex thread 2 holds/waits
# Find the mutex owner
(gdb) p ((pthread_mutex_t*)0x601090)->__data.__owner # Linux glibc mutex
# prints TID of owning thread
# Python script to dump all mutex owners (GDB 7+)
python
import gdb
for t in gdb.selected_inferior().threads():
t.switch()
print(f"Thread {t.num}: {gdb.execute('bt 3', to_string=True)}")
end
4. std::atomic の誤用パターン
// WRONG: atomic variable, but non-atomic compound operation
std::atomic<int> counter{0};
if (counter == 0) counter = 1; // not atomic together! TOCTOU race
// CORRECT: use compare_exchange
int expected = 0;
counter.compare_exchange_strong(expected, 1);
// WRONG: relaxed ordering for sync flag
std::atomic<bool> ready{false};
// Producer:
data = 42;
ready.store(true, std::memory_order_relaxed); // WRONG: no happens-before
// CORRECT: release-acquire for publishing data
// Producer:
data = 42;
ready.store(true, std::memory_order_release); // syncs with acquire
// Consumer:
if (ready.load(std::memory_order_acquire)) { // syncs with release
use(data); // safe to read data here
}
// WRONG: using data across threads without atomic/mutex
// int shared_data; // non-atomic — UB on concurrent access
// CORRECT: protect with mutex or make atomic
std::mutex mtx;
std::unique_lock lock(mtx);
shared_data = 42;
5. Happens-before の推論
C++ では、次の方法で happens-before が確立されます。
Sequenced-before (within a thread):
Statement A comes before B in code → A happens-before B
Synchronizes-with (across threads):
store(release) → load(acquire) on SAME atomic variable
→ store happens-before load
→ everything before store happens-before everything after load
Thread creation/join:
spawn(T) → any action in T (create synchronizes-with)
any action in T → join(T) (join synchronizes-before)
Mutex:
unlock(M) → lock(M) (next acquirer)
// Establishing happens-before across threads
std::atomic<int> flag{0};
int data = 0;
// Thread 1:
data = 42; // A
flag.store(1, memory_order_release); // B: A sequenced-before B
// Thread 2:
while (flag.load(memory_order_acquire) != 1) {} // C: synchronizes-with B
int x = data; // D: C sequenced-before D
// D reads 42: A happens-before B synchronizes-with C sequenced-before D
// → A happens-before D
6. Rust の並行処理 — コンパイル時の保証
Rust は所有権によって、コンパイル時にデータ競合を防ぎます。
use std::sync::{Arc, Mutex};
use std::thread;
// Shared mutable state: Arc<Mutex<T>>
let counter = Arc::new(Mutex::new(0u32));
let c = Arc::clone(&counter);
let t = thread::spawn(move || {
let mut val = c.lock().unwrap();
*val += 1;
});
t.join().unwrap();
println!("{}", *counter.lock().unwrap());
// Rust prevents:
// - Sharing &mut T across threads (Sync not impl for &mut T)
// - Moving non-Send types to threads (compiler error)
// Use TSAN_OPTIONS with cargo test if TSan checks are needed:
// RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test
関連スキル
- TSan のビルドフラグやその他のサニタイザーについては、
skills/runtimes/sanitizersを使用します - Helgrind と Memcheck の統合については、
skills/profilers/valgrindを使用します - GDB の高度なスレッド検査については、
skills/debuggers/gdbを使用します - C++/Rust のメモリ順序の理論については、
skills/low-level-programming/memory-modelを使用します
コメント
Markdown 対応