最終更新:2026-10-12
C++ デッドロック調査¶
C++で並行プログラミングのデッドロック問題をどのように調査するか?¶
最近、Apache arrowで並列対応の演算子nested loop joinを書いていました。並列が絡むと、皆さんがよく口にするデッドロック問題に遭遇します。デッドロックに遭遇したとき、どのようにデバッグ・特定すればよいのでしょうか?
これが本編の目標です。並行プログラミングのデッドロック検出とデッドロックデバッグをすばやく習得するお手伝いをします。非常に濃い内容です。それでは本題に入りましょう。
1.導入¶
デッドロックをより分かりやすく説明するために、1つのプログラムから始めます。
std::mutex gMutex;
int t2() {
std::lock_guard<std::mutex> m(gMutex);
return 0;
}
int t1() {
std::lock_guard<std::mutex> l(gMutex);
return t2();
}
このプログラムを見れば、誰もが問題があると感じるでしょう。デッドロックです!問題は、t1()関数とt2()関数の両方でグローバルのミューテックスgMutexに対してロック取得を行っている点にあります。しかし、t1()関数はロックを取得した後でt2()関数を呼び出しており、t2()関数の内部では再びgMutexに対してロックを取得しようとしています。
t1のロックはすでに取得されているのにまだ解放されておらず、そこへt2がさらにロックを取りに行きます。両者はどちらが先に解放するかを待ち続け、無限ループに入ります。実際のプロジェクトではコードはここまで単純ではなく、非常に複雑です。例えば、私がApache arrowで書いたコードは次のようなものでした:
Status OnBuildSideFinished(size_t thread_index) {
std::lock_guard<std::mutex> guard(probe_side_mutex_);
// do something
accumulate_build_ready_ = true;
return scheduler_->StartTaskGroup(thread_index,task_group_probe_,queued_batches_to_probe_.batch_count());
}
ご覧のとおり、このコードは上のシナリオよりずっと複雑で、少なくとも5層のスタックがネストされており、残りのコードはまだ貼り付けていませんが、この2つは本質的には同じデッドロックモデルです。
2.デバッグ¶
デッドロックモデルについて説明したところで、この種の問題に遭遇したとき、どのように特定するのでしょうか?
ここでは2つの方法が使えます。1つ目は、プログラムを直接実行してからgdbでアタッチする方法です。
例えば:
その後、プロセス番号を見つけて:
このとき、待機中のスレッドをいくつか取得できます。
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7fe2740 (LWP 32301) "a.out" 0x00007ffff7bc8017 in pthread_join () from /lib64/libpthread.so.0
2 Thread 0x7ffff6fd0700 (LWP 32305) "a.out" 0x00007ffff7bcd54d in __lll_lock_wait () from /lib64/libpthread.so.0
そして__lll_lock_waitのスタックを見に行きます。例えば、ここでは2番スレッドを見て、スタックを確認することでt1とt2の行番号が得られ、どこに問題があるかを直接特定できます。非常に直感的です!
この方法に加えて、gdbで直接プログラムを実行する方法もあります。この場合、プログラムは固まりますが、ctrl + cで停止させれば、上記と同じ内容が得られます。
例えば:
(gdb) r
Starting program: /home/light/a.out
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Hello World!
[New Thread 0x7ffff6fd0700 (LWP 32305)]
^C
Thread 1 "a.out" received signal SIGINT, Interrupt.
0x00007ffff7bc8017 in pthread_join () from /lib64/libpthread.so.0
Missing separate debuginfos, use: debuginfo-install libgcc-4.8.5-44.el7.x86_64 libstdc++-4.8.5-44.el7.x86_64
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7fe2740 (LWP 32301) "a.out" 0x00007ffff7bc8017 in pthread_join () from /lib64/libpthread.so.0
2 Thread 0x7ffff6fd0700 (LWP 32305) "a.out" 0x00007ffff7bcd54d in __lll_lock_wait () from /lib64/libpthread.so.0
さて、本節はここまでにします。興味のある方は、この硬核な記事のシェアをぜひお願いします!同時に、自分の**スレッドプールプロジェクト**もおすすめします。私は動画でこのプロジェクトを解説しており、dockerによるワンクリックデプロイも用意しています(この回の動画は、後ほど皆さんのために収録する予定です)。興味のある方はご連絡ください。
関連トピック¶
- C++ Concurrency in Action 読書ガイド | C++那些事
- C++ SIMD ベクトル化 | C++那些事
- C++17 並列 STL | C++那些事
- C++ False Sharing とキャッシュライン | C++那些事
- GDB マルチスレッド All-Stop デバッグ | C++那些事
- Helgrind 並行エラー検出 | C++那些事
- libev イベントループの原理 | C++那些事
- C++ std::thread の内部実装 | C++那些事
- C++ std::thread と jthread | C++那些事
- C++ マルチスレッド入門 | C++那些事