gdbでのマルチスレッド処理のデバッグや制御について

 マルチスレッド処理のデバッグや解析において、gdbで各スレッドの実行・停止を制御する操作についてメモ。
 なお今回は解説で以下のサンプルコードを使用する。ここでは3つのスレッドがそれぞれ「m_count 」「t1_count 」「t2_count 」の3つの変数をインクリメントしている。

//main.c
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

static unsigned int m_count = 0, t1_count = 0, t2_count = 0;

void *thread1(void *args)
{
    while (1) {
        t1_count++;
    }
    return NULL;
}

void *thread2(void *args)
{
    while (1) {
        t2_count++;
    }
    return NULL;
}

int main(void)
{
    int i;
    pthread_t t1, t2;
    
    pthread_create(&t1, NULL, thread1, (void *)NULL);
    pthread_create(&t2, NULL, thread2, (void *)NULL);
    while (1) {
        m_count++;
    }
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("exit\n");
    return 0;
}

 gdbの基本操作は今回は省略する。一応、マルチスレッド処理でスレッド単位で各種制御を行う場合の主要操作は以下の通り。

  • 「break [linespec] thread [threadno]」で特定のスレッドのみに有効なbreakpointを設定
  • 「thread apply [threadno] [command]」で特定スレッドに対しcommand実行。
  • 「thread [threadno]」でカレントスレッド切り替え。

 上記のうち「threadno」は「info threads」で取得できる。また[threadno]に「all」を指定すると全スレッドに対する一括操作ができる。

全スレッドの停止・実行をgdbで一括制御する

 まずスレッドを区別せず、全スレッドに対して一括してbreak処理、実行処理を行いたい場合だけれど、これはデフォルト設定(scheduler-locking off設定)でできる。
 上記サンプルコードをgdbで実行させた際の結果をまとめる。



(下記では無関係な出力は極力削除している)
localhost:mulit_thread goyoki$ gcc main.c -g ←ビルドしgdb起動
localhost:mulit_thread goyoki$ gdb a.out
(gdb) list
9 while (1) {
10 t1_count++;
11 }
12 return NULL;
13 }
(gdb) break 10      ←thread1のt1_countのインクリメント処理にブレークポイント設置
Breakpoint 1 at 0x100000e38: file main.c, line 10.
(gdb) ignore 1 100 ←100回程度インクリメント処理を実行するように設定
Will ignore next 100 crossings of breakpoint 1.
(gdb) run
Breakpoint 1, thread1 (args=0x0) at main.c:10 ←実行開始後、ブレークポイントで停止状態に移る
10 t1_count++;
(gdb) print m_count    ←以下インクリメントしている変数を表示
$1 = 387368
(gdb) print t1_count
$2 = 100
(gdb) print t2_count
$3 = 410726
(gdb) print m_count    ←しばらく時間をおいて、インクリメントしている変数を表示。全スレッドが停止しているのがわかる
$4 = 387368
(gdb) print t1_count
$5 = 100
(gdb) print t2_count
$6 = 410726
(gdb) ignore 1 100    ←100回程度インクリメント処理を実行するようにまた設定して、停止状態から再実行
Will ignore next 100 crossings of breakpoint 1.
(gdb) c
Continuing.

Breakpoint 1, thread1 (args=0x0) at main.c:10
10 t1_count++;  ←以下インクリメントしている変数を表示。すべてのスレッドが動作し、全変数がインクリメントされている
(gdb) print m_count
$7 = 759039
(gdb) print t1_count
$8 = 201
(gdb) print t2_count
$9 = 798867
(gdb)


 上記のように、thread1で停止→再実行のコマンドを実行した際は、全スレッドがthread1に合わせて停止、再実行されている。

gdbで特定のスレッドのみ停止・実行するように制御する

 マルチスレッド処理で、特定のスレッドのみ実行し、それ以外のスレッドを停止状態にしたい場合は「scheduler-locking on」設定を行う。その設定を有効化した場合のgdb出力を以下にまとめる。


(下記では無関係な出力は極力削除している)
localhost:multi_thread goyoki$ gcc main.c -g
localhost:multi_thread goyoki$ gdb a.out
(gdb) break 10  ←thread1のt1_countのインクリメント処理にブレークポイント設置
Breakpoint 1 at 0x100000e38: file main.c, line 10.
(gdb) ignore 1 100   ←100回程度インクリメント処理を行うようにする
Will ignore next 100 crossings of breakpoint 1.
(gdb) run
Breakpoint 1, thread1 (args=0x0) at main.c:10
10 t1_count++;     ←停止状態での3つの変数を表示
(gdb) print m_count
$1 = 851417
(gdb) print t1_count
$2 = 100
(gdb) print t2_count
$3 = 820502
(gdb) set scheduler-locking on    ←scheduler-locking on設定
(gdb) ignore 1 100     ←thread1で変数を100回インクリメント処理するように設定して再実行
Will ignore next 100 crossings of breakpoint 1.
(gdb) c
Continuing.

Breakpoint 1, thread1 (args=0x0) at main.c:10
10 t1_count++;
(gdb) print m_count    ←各変数を表示させる。thread1のみ実行されていて、他スレッドは最初の停止状態から停止されたままなのがわかる
$4 = 851417
(gdb) print t1_count
$5 = 201
(gdb) print t2_count
$6 = 820502
(gdb)


 上記のように、「scheduler-locking on」設定を有効にすると、停止状態から再実行した際、操作したスレッドのみが再実行され、それ以外のスレッドは停止状態のままとなる。
 
 注意として、scheduler-locking設定の動作はOS依存で、OSによっては設定できない場合がある。
 またscheduler-lockingをon設定すると、内部的にマルチスレッド制御を行なっているAPIや標準関数がおかしな挙動を示すことがある。例えばnanosleep()などは正常に動かないことがある。
 あと「3つ以上のスレッドで一つのスレッドのみ実行、それ以外は停止状態」はできても「3つ以上のスレッドで一つのスレッドのみ停止状態にし、それ以外は実行」をするのはOS機能依存で、一般的に難しい。

その他(dead lockの解析など)

 gdbはdead lockの解析でかなり使えることがある。例えばソフトウェアがdead lockで停止した場合は、以下の操作でどこでlockしているのか解析できる。

  1. gdb起動
  2. 「attach [PID]」でdead lockを起こしているプロセスにアタッチ
  3. 「thread apply all bt」で全スレッドのコールスタックを取得
  4. 具体的にファイルのどの位置でdead lockを起こしているか解析したい場合は、得られたコールスタックのアドレスを「addr2line」 でファイル行番号情報に変換

最後に

 なお、gdbでマルチスレッド処理のデバッグを行うのはやっぱり手間がかかる。
 細かなスレッド制御でデバッグを行いたいなら、「ブレークポイントの設置が容易になるよう、表明やログ出力など異常発生時に実行されるコードを充実させる」「実行中に細かなスレッド制御ができるように、スレッドコントローラをソフトウェアの内部機能として組み込んでおく」といった工夫を行ったほうが無難だと思う。