C#でデッドロックするアプリをデバッグする

Posted by Yutaka Hara on May 16, 2022 · 1 min read

こんにちは。NaCl松江本社の原です。Ruby案件が多い弊社ですが、時にはそれ以外の言語を扱うこともあります。今回はC#の話を。

Visual Studioでは「プロセスにアタッチ」という機能を使うとexeとして起動したあとでもデバッガを起動してスレッド一覧を見たりできるのですが、その手順がなかなかわからなかったので、メモとして書き残しておきます。

サンプルプログラム

例として、デッドロックを起こすプログラムを用意しました。画面にはLabelが一つ、Buttonが一つあります。プログラムを起動するとカウンタが少しずつ増えていき、ボタンを押すと0にリセットされます。

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        int ct = 0;
        Object ctLock = new Object();

        public MainWindow()
        {
            InitializeComponent();
            Task.Run(CountUp);
        }

        void CountUp()
        {
            while (true)
            {
                Thread.Sleep(1);
                lock (ctLock)
                {
                    ct++;
                    Dispatcher.Invoke(() =>
                    {
                        Label1.Content = ct.ToString();
                    });
                }
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            lock (ctLock)
            {
                ct = 0;
            }
        }
    }
}

1つのプログラム内にロックするリソースが複数ある場合、どの順番でロックするかをプログラム全体で統一しないと、デッドロックする可能性があります。lockによるロック以外にも、例えばUIスレッドでの処理は同時に一つしか実行できないため同様に気をつける必要があります。

この例では、

  • CountUpは
    • (1) ctLockをロック
    • (2) Dispatcher.InvokeでUIスレッドでの処理を要求し、終わるまで待つ
  • Button_Clickは
    • (1) イベントハンドラなので、UIスレッド内で呼ばれる
    • (2) ctLockをロック

のように順序が食い違っているため、ボタンを押したときに運が悪いと以下の状態になり、デッドロックします。

  • UIスレッド
    • ctLockのロック開放待ち
  • CountUpのスレッド
    • ctLockのロックを取ったあと、UIスレッドでの処理終了待ち

(余談ですが、このようなケースを避けるため、終了を待つ必要がなければDispatcher.InvokeAsyncを使う方が良いでしょう)

プロセスにアタッチ

ではVisual Studioでこのexeをデバッグしてみます。とその前に、スレッド一覧を見るにはブレークポイントを仕掛ける必要があります。デッドロック箇所の見当がついていればいいですが、そうでないときは以下のように「Thread.Sleepするだけのスレッド」を作ってそこにブレークポイントを仕掛けましょう。

exeを起動し、ボタンを連打してデッドロック状態にします。次に、「デバッグ」→「プロセスにアタッチ」から当該のexeを選択します。ブレークポイントが適切に設定できていれば、そこを通った瞬間にプロセスの動作が止まり、「デバッグ」→「ウィンドウ」→「並列スタック」からスレッドの一覧を見ることができます。

こんな感じで各スレッドのスタックトレースが表示され、どのスレッドが何をやってるのかかが分かります。便利ですね。