「デッドロックが検出されました」というメッセージ、見たことありますか?
マルチスレッド処理やデータベース操作をしていると、ある日突然このエラーが表示されて、プログラムが止まってしまう…。
初心者の方はもちろん、ある程度経験のあるエンジニアでも、最初はかなり戸惑うのではないでしょうか。
私自身、業務でマルチスレッド処理を扱ったとき、何度もこのエラーに悩まされました。
ログを見てもわかりにくく、原因を突き止めるまでにずいぶん時間がかかったのを覚えています。
この記事では、C#でデッドロックが発生する典型的な原因と、その正しい対処法・防止策を、実例を交えながらわかりやすく解説していきます。
「デッドロックってそもそも何?」というところから丁寧に説明しますので、どうぞ肩の力を抜いて読み進めてください。
デッドロックとは?
まずは、「デッドロック」そのものがどういう状態かをしっかり押さえておきましょう。
簡単に言うと、お互いが相手の持っている「鍵(リソース)」を待ち続けて、永遠に進めなくなってしまう状態のことを「デッドロック(Deadlock)」と呼びます。
イメージで理解しよう:二人の人間と2つの鍵

たとえば、こんな状況を思い浮かべてみてください。
どちらも相手の鍵を持っていないと次に進めないのですが、お互いに鍵を渡さずに待ち続けたまま、永遠に進めなくなる。
まさにこれが「デッドロック」なのです。
C#におけるデッドロックの典型例
C#でこのような状態が起きるのは、主に次の2パターンです。
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 完了");
}
}
}
何が問題なのか?
この状態で 両スレッドが同時に動くと、互いに相手のロックが開放されるのを待ち続けて止まってしまう――これがデッドロックです。

ロックの取得順序がバラバラだと、こうした状況になりやすいですね。
データベースアクセスでおきるデッドロック(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におけるインデックスの重要性や、エラーメッセージの読み解き方など、実践的な内容も取り上げました。
コメント