【C#】「デッドロックが検出されました」の原因と正しい対処法をわかりやすく解説

スポンサーリンク

「デッドロックが検出されました」というメッセージ、見たことありますか?

マルチスレッド処理やデータベース操作をしていると、ある日突然このエラーが表示されて、プログラムが止まってしまう…。
初心者の方はもちろん、ある程度経験のあるエンジニアでも、最初はかなり戸惑うのではないでしょうか。

私自身、業務でマルチスレッド処理を扱ったとき、何度もこのエラーに悩まされました。
ログを見てもわかりにくく、原因を突き止めるまでにずいぶん時間がかかったのを覚えています。

この記事では、C#でデッドロックが発生する典型的な原因と、その正しい対処法・防止策を、実例を交えながらわかりやすく解説していきます。
「デッドロックってそもそも何?」というところから丁寧に説明しますので、どうぞ肩の力を抜いて読み進めてください。

デッドロックとは?

まずは、「デッドロック」そのものがどういう状態かをしっかり押さえておきましょう。

簡単に言うと、お互いが相手の持っている「鍵(リソース)」を待ち続けて、永遠に進めなくなってしまう状態のことを「デッドロック(Deadlock)」と呼びます。

イメージで理解しよう:二人の人間と2つの鍵

たとえば、こんな状況を思い浮かべてみてください。

  • Aさんは「部屋1の鍵」を持っています。
  • Bさんは「部屋2の鍵」を持っています。
  • Aさんは部屋2に入りたい。
  • Bさんは部屋1に入りたい。

どちらも相手の鍵を持っていないと次に進めないのですが、お互いに鍵を渡さずに待ち続けたまま、永遠に進めなくなる
まさにこれが「デッドロック」なのです。

C#におけるデッドロックの典型例

C#でこのような状態が起きるのは、主に次の2パターンです。

  1. マルチスレッドでのロックの競合
    lock文やMonitorなどで、複数のスレッドが同時にロックを取りに行ったときに発生
  2. データベースアクセス時のトランザクション競合
    たとえば、SQL Serverなどに対して複数のトランザクションが同時に異なる順番でロックを取得しようとして、相互に待ち状態になるケース

C#でデッドロックが発生するよくあるシチュエーション

デッドロックは「理屈ではわかってるけど、実際にどんなときに起きるの?」という疑問を持つ方が多いです。
ここでは、C#でありがちな2つの代表的なシーンを紹介します。

マルチスレッド処理で起きるデッドロック

object lockA = new object();
object lockB = new object();

void Thread1()
{
    lock (lockA)
    {
        Thread.Sleep(100); // 少し待ってから
        lock (lockB)
        {
            Console.WriteLine("Thread1 完了");
        }
    }
}

void Thread2()
{
    lock (lockB)
    {
        Thread.Sleep(100); // 少し待ってから
        lock (lockA)
        {
            Console.WriteLine("Thread2 完了");
        }
    }
}

何が問題なのか?

  • Thread1 は lockA を取得後に lockB を取りに行く
  • Thread2 は逆に lockBlockA の順でロックを取得しようとする

この状態で 両スレッドが同時に動くと、互いに相手のロックが開放されるのを待ち続けて止まってしまう――これがデッドロックです。

なんくる
なんくる

ロックの取得順序がバラバラだと、こうした状況になりやすいですね。

データベースアクセスでおきるデッドロック(SQL Serverの例)

例えば、こんなコードです。

using (var connection = new SqlConnection(connStr))
{
    connection.Open();
    using (var transaction = connection.BeginTransaction())
    {
        var command1 = new SqlCommand("UPDATE Orders SET Status = 'Processing' WHERE Id = 1", connection, transaction);
        var command2 = new SqlCommand("UPDATE Customers SET Status = 'Busy' WHERE Id = 2", connection, transaction);

        command1.ExecuteNonQuery();
        command2.ExecuteNonQuery();

        transaction.Commit();
    }
}

一見なんてことない処理ですが、別のトランザクションが逆順(Customers → Orders)で更新していると、これまたデッドロックになります。

⚠️なぜ起きるの?

  • 両者が「異なる順序」でテーブルに対してロックを取得しようとしているから
  • トランザクションが長引いたり、インデックスがなかったりすると、競合の可能性がさらに高くなります

デッドロックの対処法

デッドロックは「一度ハマると抜け出せない厄介なバグ」ですが、適切な設計と少しの工夫で未然に防ぐことができます。
ここでは、実践的な対処法をシーン別に紹介します。

マルチスレッド処理での対処法

対処法1:ロックの取得順序を統一する

最も基本かつ効果的な方法です。
すべてのスレッドでロックを取得する順番を同じにするだけで、ほとんどのデッドロックは回避できます。

object lockA = new object();
object lockB = new object();

void SafeMethod()
{
    object[] locks = new[] { lockA, lockB }.OrderBy(l => l.GetHashCode()).ToArray();
    lock (locks[0])
    {
        lock (locks[1])
        {
            // 安全な処理
        }
    }
}
なんくる
なんくる

OrderBy を使ってロック順序を固定するのは、実務でもよく使われる手法です。

対処法2:Monitor.TryEnterでタイムアウト付きロック

どうしても順序の制御が難しい場合、ロック取得にタイムアウトを設けることで、永久待ちを防げます。

if (Monitor.TryEnter(lockA, TimeSpan.FromSeconds(1)))
{
    try
    {
        // lockA の中での処理
    }
    finally
    {
        Monitor.Exit(lockA);
    }
}
else
{
    Console.WriteLine("lockAの取得に失敗しました。");
}

対処法3:SemaphoreSlim を使ったリソース制御

lock より柔軟に扱える SemaphoreSlim を使うのも有効です。
特に非同期環境(async/await)と相性が良いため、最近のアプリ開発ではよく利用されます。

データベース処理での対処法

データベース側での対処法もまとめておきます。

対処法1:アクセス順序の統一

テーブルを更新する順番がバラバラだと、デッドロックの温床になります。
すべてのトランザクションで 更新対象のテーブルを同じ順序で処理するように設計しましょう。

対処法2:トランザクションを短く保つ

トランザクション中に長い処理(待機・ユーザー操作など)があると、ロック保持時間が長くなり、デッドロックのリスクが高まります。
「Begin → 最小限の処理 → Commit」が基本です。

まとめ

C#で「デッドロックが検出されました」というエラーに出会ったとき、最初は驚くかもしれません。
でも、その正体は「お互いがリソースの解放を待ち続けて、永久に進めなくなってしまう状態」なんです。

この記事では、マルチスレッドやデータベースアクセスの場面でデッドロックがどうやって発生するのかを具体例とともに解説しました。
また、それに対処するための設計の工夫やコードレベルでの対策(ロック順序の統一、リトライ処理など)も紹介しました。

さらに、SQL ServerやPostgreSQLにおけるインデックスの重要性や、エラーメッセージの読み解き方など、実践的な内容も取り上げました。

C#
スポンサーリンク
なんくるをフォローする

コメント

タイトルとURLをコピーしました