デッドロックは、複数のスレッドが互いに相手のリソースの解放を待っている状態で、どのスレッドも進行できなくなる問題です。ここでは、C#でデッドロックを起こすサンプルコードとその対策版を紹介します。
デッドロックが発生するサンプルコード
以下のサンプルコードは、スレッド間でのリソース共有に関する問題を示しています。この例では、タスク1とタスク2を並列実行します。リソースにアクセスする順番は、あえてタスクごとに変えてあります。このプログラムを実行すると、どのような結果になるでしょうか。
using System;
using System.Threading;
class Program
{
static int ResourceA = 0; // 共有リソースA
static int ResourceB = 0; // 共有リソースB
static readonly object lockObjA = new object(); // 共有リソースAのロック用オブジェクト
static readonly object lockObjB = new object(); // 共有リソースBのロック用オブジェクト
static void Main(string[] args)
{
// スレッドの作成と開始: タスク1
Thread thread1 = new Thread(new ThreadStart(Task1));
thread1.Start();
// スレッドの作成と開始: タスク2
Thread thread2 = new Thread(new ThreadStart(Task2));
thread2.Start();
// 両スレッドの終了をメインスレッドが待機
thread1.Join();
thread2.Join();
Console.WriteLine($"Final resource value: {ResourceA}, {ResourceB}");
}
// タスク1: カウンタを100万回インクリメント
static void Task1()
{
for (int i = 0; i < 1000000; i++)
{
lock (lockObjA) // ★共有リソースAの編集を1スレッドに限定する
{
ResourceA++; // 共有リソースAの編集
lock (lockObjB) // ★共有リソースBの編集を1スレッドに限定する
{
ResourceB++; // 共有リソースBの編集
}
}
}
}
// タスク2: カウンタを100万回デクリメント
static void Task2()
{
for (int i = 0; i < 1000000; i++)
{
lock (lockObjB) // ★共有リソースBの編集を1スレッドに限定する
{
ResourceB--; // 共有リソースBの編集
lock (lockObjA) // ★共有リソースAの編集を1スレッドに限定する
{
ResourceA--; // 共有リソースAの編集
}
}
}
}
}
実行結果:プログラムは終了せず、実行状態のままとなる。
問題点
タスク1は、リソースA→Bの順番で処理を行っています。このとき、lockステートメントもA→Bの順番にネストして行っています。
一方で、タスク2は、リソースB→Aの順番で処理を行っています。このとき、lockステートメントもB→Aの順番にネストして行っています。
このような書き方をすると、たとえばタスク1がAのロックを取得している状態で、タスク2がロックBを取得する状態となる場合があります。このとき、タスク1は次にBのロックを取ろうとしますが、Bはタスク2がすでにロックを取得しているので、タスク1は待たされることになります。タスク2も、Aのロックを取得しようとして、タスク1がAの解放をするまで待つことになります。
このように、互いがリソース解放待ちとなってしまうことで、プログラムが進行不可能になることをデッドロックと言います。
対策版
問題を解決するためには、共有リソースAとBのアクセスの順番を揃える必要があります。以下のサンプルコードでは、タスク1と2共にリソースA→Bの順番にアクセスするよう変更しています。
using System;
using System.Threading;
class Program
{
static int ResourceA = 0; // 共有リソースA
static int ResourceB = 0; // 共有リソースB
static readonly object lockObjA = new object(); // 共有リソースAのロック用オブジェクト
static readonly object lockObjB = new object(); // 共有リソースBのロック用オブジェクト
static void Main(string[] args)
{
// スレッドの作成と開始: タスク1
Thread thread1 = new Thread(new ThreadStart(Task1));
thread1.Start();
// スレッドの作成と開始: タスク2
Thread thread2 = new Thread(new ThreadStart(Task2));
thread2.Start();
// 両スレッドの終了をメインスレッドが待機
thread1.Join();
thread2.Join();
Console.WriteLine($"Final resource value: {ResourceA}, {ResourceB}");
}
// タスク1: カウンタを100万回インクリメント
static void Task1()
{
for (int i = 0; i < 1000000; i++)
{
lock (lockObjA) // ★共有リソースAの編集を1スレッドに限定する
{
ResourceA++; // 共有リソースAの編集
lock (lockObjB) // ★共有リソースBの編集を1スレッドに限定する
{
ResourceB++; // 共有リソースBの編集
}
}
}
}
// タスク2: カウンタを100万回デクリメント
static void Task2()
{
for (int i = 0; i < 1000000; i++)
{
lock (lockObjA) // ★共有リソースAの編集を1スレッドに限定する
{
ResourceA--; // 共有リソースAの編集
lock (lockObjB) // ★共有リソースBの編集を1スレッドに限定する
{
ResourceB--; // 共有リソースBの編集
}
}
}
}
}
実行結果:プログラムは実行完了し、結果が表示されるようになった。
Final resource value: 0, 0
続行するには何かキーを押してください . . .
まとめ
本記事では、スレッド間でリソースを共有する際に発生するデッドロックの問題について紹介しました。複数の共有リソースにアクセスする場合は、リソースの取得順序を一貫させることで避けることが可能です。