1. 相互排他とリソース管理
マルチスレッドプログラミングにおいて、複数のスレッドが共有リソース(クリティカルリソース)にアクセスする際には、データの整合性を保つための同期メカニズムが不可欠です。一般的に、個々のクリティカルリソースにはそれぞれ対応するロック(ミューテックスなど)を割り当て、排他的なアクセスを保証します。
2. デッドロックの概念と発生条件
デッドロックとは、複数のスレッドが互いにリソースの解放を待ち合わせる状態に陥り、どのスレッドも処理を続行できなくなる状況を指します。
2.1 デッドロックの発生条件
デッドロックが発生するには、以下の四つの条件が同時に成立する必要があります。
- 相互排他 (Mutual Exclusion): システム内に複数のクリティカルリソースが存在し、各リソースは一度に一つのスレッドしか使用できない。
- 保持と待機 (Hold and Wait): あるリソースを保持しているスレッドが、別のリソースの解放を待機している。
- 非割込み (No Preemption): スレッドが保持しているリソースは、そのスレッドが自発的に解放するまで、他のスレッドによって強制的に割り当てを解除されることはない。
- 循環待機 (Circular Wait): 複数のスレッドが環状にリソースを待機している。例えば、スレッドAがリソースBを待ち、スレッドBがリソースCを待ち、スレッドCがリソースAを待つような状態。
2.2 デッドロックの実例
以下のQt C++コードは、異なる順序で2つのミューテックスを取得しようとする2つのスレッドがどのようにデッドロックに陥るかを示しています。
#include <QCoreApplication>
#include <QThread>
#include <QMutex>
#include <QDebug>
// 共有リソースを保護するためのミューテックス
static QMutex resourceALock;
static QMutex resourceBLock;
// クリティカルリソースを異なる順序でロックするスレッドA
class ConcurrentTaskA : public QThread
{
protected:
void run() override
{
while (true)
{
resourceALock.lock(); // リソースAをロック
qDebug() << objectName() << ": リソースAをロックしました。リソースBを待機中...";
resourceBLock.lock(); // リソースBをロック (デッドロックの可能性あり)
qDebug() << objectName() << ": リソースAとBの両方をロックし、作業を実行中。";
// ... ここでクリティカルセクションの作業を行う ...
resourceBLock.unlock();
resourceALock.unlock();
QThread::msleep(100); // 短時間スリープ
}
}
};
// クリティカルリソースを異なる順序でロックするスレッドB
class ConcurrentTaskB : public QThread
{
protected:
void run() override
{
while (true)
{
resourceBLock.lock(); // リソースBをロック
qDebug() << objectName() << ": リソースBをロックしました。リソースAを待機中...";
resourceALock.lock(); // リソースAをロック (デッドロックの可能性あり)
qDebug() << objectName() << ": リソースBとAの両方をロックし、作業を実行中。";
// ... ここでクリティカルセクションの作業を行う ...
resourceALock.unlock();
resourceBLock.unlock();
QThread::msleep(100); // 短時間スリープ
}
}
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
ConcurrentTaskA taskA;
ConcurrentTaskB taskB;
taskA.setObjectName("タスクA");
taskB.setObjectName("タスクB");
taskA.start();
taskB.start();
return app.exec();
}
上記のコードでは、ConcurrentTaskAはresourceALockを先に取得し、次にresourceBLockを取得しようとします。一方、ConcurrentTaskBはresourceBLockを先に取得し、次にresourceALockを取得しようとします。この異なるロック順序により、両方のスレッドが相手が保持しているリソースを待つ状態となり、デッドロックが発生する可能性が非常に高くなります。
3. デッドロックの回避戦略
デッドロックを回避するための一つの有効な方法は、資源要求における循環待機条件を排除することです。
3.1 資源の順序付けと要求規則
具体的な戦略は以下の通りです。
- システム内の全てのクリティカルリソースに、ユニークな識別番号(例: r1, r2, ..., rn)を割り当てます。
- 対応するスレッドロック(例: m1, m2, ..., mn)にも同じ順序で識別番号を割り当てます。
- システム内の全てのスレッドは、リソースを要求する際に、その識別番号の厳密な昇順に従ってロックを取得します。
3.2 デッドロック回避の実例
以下のコードは、上記のデッドロック例を修正し、デッドロックを回避する方法を示しています。両方のスレッドが同じ順序でミューテックスを要求します。
#include <QCoreApplication>
#include <QThread>
#lt;QMutex>
#include <QDebug>
static QMutex orderedResource1; // リソース1に対応するロック
static QMutex orderedResource2; // リソース2に対応するロック
// リソースを昇順でロックするスレッドX
class SafeTaskX : public QThread
{
protected:
void run() override
{
while (true)
{
orderedResource1.lock(); // 常にリソース1を先にロック
qDebug() << objectName() << ": リソース1をロックしました。リソース2を待機中...";
orderedResource2.lock(); // 次にリソース2をロック
qDebug() << objectName() << ": リソース1と2の両方をロックし、作業を実行中。";
// ... クリティカルセクションの作業 ...
orderedResource2.unlock();
orderedResource1.unlock();
QThread::msleep(100);
}
}
};
// リソースを昇順でロックするスレッドY
class SafeTaskY : public QThread
{
protected:
void run() override
{
while (true)
{
orderedResource1.lock(); // 常にリソース1を先にロック
qDebug() << objectName() << ": リソース1をロックしました。リソース2を待機中...";
orderedResource2.lock(); // 次にリソース2をロック
qDebug() << objectName() << ": リソース1と2の両方をロックし、作業を実行中。";
// ... クリティカルセクションの作業 ...
orderedResource2.unlock();
orderedResource1.unlock();
QThread::msleep(100);
}
}
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
SafeTaskX taskX;
SafeTaskY taskY;
taskX.setObjectName("セーフタスクX");
taskY.setObjectName("セーフタスクY");
taskX.start();
taskY.start();
return app.exec();
}
この修正されたコードでは、SafeTaskXとSafeTaskYの両方が、常にorderedResource1を先にロックし、次にorderedResource2をロックします。これにより、循環待機条件が解消され、デッドロックが発生する可能性を排除できます。
4. セマフォの活用
セマフォは、ミューテックスよりも柔軟な同期メカニズムであり、複数のスレッドが同時にクリティカルリソースにアクセスすることを許可します。これは、アクセス可能なリソースの数をカウントすることで実現されます。
4.1 セマフォの特性
- セマフォは特別な種類のスレッドロックです。
- セマフォは、許可されたスレッド数まで、複数のスレッドが同時にクリティカルリソースにアクセスすることを可能にします。
- Qtフレームワークは
QSemaphoreクラスを通じてセマフォを直接サポートしています。
4.2 セマフォを用いた生産者-消費者問題の解決
以下のQt C++コードは、セマフォを使用して複数の生産者スレッドと消費者スレッドが共有バッファを安全に利用する生産者-消費者問題の解決策を示しています。
#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QSemaphore>
#include <QMutex>
#include <QVector>
#include <QRandomGenerator> // 適切な乱数生成器を使用
const int BUFFER_CAPACITY = 5; // バッファの最大容量
// 共有バッファ
QVector<int> sharedItemBuffer(BUFFER_CAPACITY, 0); // 0は空のスロットを示す
// 空きスロットの数を管理するセマフォ (初期値はバッファ容量)
QSemaphore emptySlotsSemaphore(BUFFER_CAPACITY);
// 満たされたスロットの数を管理するセマフォ (初期値は0)
QSemaphore filledSlotsSemaphore(0);
// 共有バッファへのアクセスを保護するためのミューテックス
QMutex bufferAccessMutex;
// アイテムを生成し、バッファに追加する生産者スレッド
class ItemProducer : public QThread
{
protected:
void run() override
{
while (true)
{
int newItem = QRandomGenerator::global()->bounded(100, 1000); // 100-999のランダムな値
// 空きスロットが利用可能になるまで待機し、カウントを減らす
emptySlotsSemaphore.acquire();
bufferAccessMutex.lock(); // バッファへのアクセスをロック
for (int i = 0; i < BUFFER_CAPACITY; ++i)
{
if (sharedItemBuffer[i] == 0) // 空きスロットを見つける
{
sharedItemBuffer[i] = newItem;
qDebug() << objectName() << "がアイテムを生成: {" << i << ", " << newItem << "}";
break;
}
}
bufferAccessMutex.unlock(); // バッファへのアクセスをアンロック
// 満たされたスロットの数を増やす
filledSlotsSemaphore.release();
QThread::sleep(1); // 1秒スリープ
}
}
};
// バッファからアイテムを消費する消費者スレッド
class ItemConsumer : public QThread
{
protected:
void run() override
{
while (true)
{
// 満たされたスロットが利用可能になるまで待機し、カウントを減らす
filledSlotsSemaphore.acquire();
bufferAccessMutex.lock(); // バッファへのアクセスをロック
for (int i = 0; i < BUFFER_CAPACITY; ++i)
{
if (sharedItemBuffer[i] != 0) // 満たされたスロットを見つける
{
int consumedItem = sharedItemBuffer[i];
sharedItemBuffer[i] = 0; // スロットを空としてマーク
qDebug() << objectName() << "がアイテムを消費: {" << i << ", " << consumedItem << "}";
break;
}
}
bufferAccessMutex.unlock(); // バッファへのアクセスをアンロック
// 空きスロットの数を増やす
emptySlotsSemaphore.release();
QThread::sleep(2); // 2秒スリープ
}
}
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
ItemProducer producer1, producer2;
producer1.setObjectName("生産者1");
producer2.setObjectName("生産者2");
ItemConsumer consumer1;
consumer1.setObjectName("消費者1");
producer1.start();
producer2.start();
consumer1.start();
return app.exec();
}
この生産者-消費者モデルでは、emptySlotsSemaphoreはバッファに空きがあることを示し、filledSlotsSemaphoreはバッファに消費可能なアイテムがあることを示します。生産者はemptySlotsSemaphoreを取得してバッファにアイテムを追加し、filledSlotsSemaphoreを解放します。消費者はfilledSlotsSemaphoreを取得してバッファからアイテムを取り出し、emptySlotsSemaphoreを解放します。また、bufferAccessMutexは、複数のスレッドが同時にバッファの同じスロットにアクセスしてデータ競合を引き起こすのを防ぎます。