はじめに
排他制御とは、複数のプロセスやスレッドが、メモリやファイルといった共有リソースに同時にアクセスする際に、データの整合性を保つための仕組みや手法のことを指します。排他制御の目的は、共有リソースに対する競合状態を防ぎ、予期しない動作やデータの破損を避けることです。
排他制御の代表的な手法には以下のようなものがありますが、この記事ではMutexについて解説し、実際の使い方を紹介したいと思います。
- Mutex
- セマフォ
- リード・ライトロック
- スピンロック
排他制御は、特にマルチスレッドプログラミングや並列処理において重要な概念です。正しく実装されないと、デッドロック(プロセスやスレッドが相互に待ち状態に陥る状態)やスタベーション(あるスレッドが永久にリソースを得られない)などの問題が発生する可能性があります。そのため、排他制御のメカニズムを適切に理解し、正しく活用することが重要です。
C++でマルチスレッドプログラミングをする方法は以下の記事で紹介していますので、マルチスレッドがどういうものかわからない方はご覧ください。
Mutexとは
Mutexは、最も一般的な排他制御手法です。「Mutual Exclusion」の略で、一度に一つのスレッドだけがリソースにアクセスできるようにするロック機構です。あるスレッドがミューテックスを取得すると、他のスレッドはそのミューテックスが解放されるまで待つ必要があります。
ミューテックスの基本操作
ロック(lock)
- 他のスレッドがMutexのロックを取得していない場合、ロックを取得し、クリティカルセクションに入ります。クリティカルセッションとは、プログラム内で一度に一つのスレッドだけが実行できるコードの部分を指します。そのため、クリティカルセッションの中でのみ共有リソースの操作を行うことで、排他制御を実現することができます。
- Mutexが既にロックされている場合、そのスレッドはMutexが解放されるまで待機します。
アンロック(unlock)
- スレッドがクリティカルセクションを出る際にMutexを解放します。これにより、他の待機中のスレッドがMutexを取得できるようになります。
サンプルコード
C++の標準ライブラリ<mutex>
を使って、簡単な排他制御のプログラムを書いてみましょう。
2つのスレッドによるカウンタのインクリメント
以下のコードでは、2つのスレッドが共有のカウンタをインクリメントします。、2つのスレッドが同時にカウンタをインクリメントするのを防ぐためにMutexを使用します。
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0; // 共有カウンタ
std::mutex counterMutex; // ミューテックス
void incrementCounter(int id) {
for (int i = 0; i < 100; ++i) {
// ミューテックスのロック
std::lock_guard<std::mutex> lock(counterMutex);
int temp = counter; // カウンタの現在値を読み込む
std::this_thread::sleep_for(std::chrono::microseconds(1)); // 遅延を追加
counter = temp + 1; // カウンタをインクリメント
std::cout << "Thread " << id << " incremented counter to " << counter << std::endl;
} // lock_guardがスコープを抜けるとミューテックスは自動的にアンロックされる
}
int main() {
std::thread thread1(incrementCounter, 1);
std::thread thread2(incrementCounter, 2);
thread1.join();
thread2.join();
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
ヘッダーのインクルード
#include <iostream>
#include <thread>
#include <mutex>
<iostream>
は標準入出力ストリームを扱うため、<thread>
はマルチスレッドを扱うため、<mutex>
はMutexを扱うために使用します。
共有リソースとミューテックスの定義
int counter = 0; // 共有カウンタ
std::mutex counterMutex; // ミューテックス
counter
は共有されるカウンタ変数で、counterMutex
はこのカウンタを保護するためのMutexです。
スレッド関数の定義
void incrementCounter(int id) {
for (int i = 0; i < 100; ++i) {
// ミューテックスのロック
std::lock_guard<std::mutex> lock(counterMutex);
int temp = counter; // カウンタの現在値を読み込む
std::this_thread::sleep_for(std::chrono::microseconds(1)); // 遅延を追加
counter = temp + 1; // カウンタをインクリメント
std::cout << "Thread " << id << " incremented counter to " << counter << std::endl;
} // lock_guardがスコープを抜けるとミューテックスは自動的にアンロックされる
}
incrementCounter
関数は、指定された回数カウンタをインクリメントします。std::lock_guard
を使って、Mutexを自動的にロックおよびアンロックします。lock_guard
がスコープを抜けると自動的にアンロックされるため、安全に排他制御が行えます。
int temp = counter; // カウンタの現在値を読み込む
std::this_thread::sleep_for(std::chrono::microseconds(1)); // 遅延を追加
counter = temp + 1; // カウンタをインクリメント
この部分では、スレッド間での競合を発生しやすくするために、あえて無駄な処理をしたり遅延を入れたりしています。
スレッドの作成と開始
std::thread thread1(incrementCounter, 1);
std::thread thread2(incrementCounter, 2);
2つのスレッドを作成し、それぞれにincrementCounter
関数を渡して開始しています。第2引数はスレッドIDです。
スレッドの完了を待機
thread1.join();
thread2.join();
join()
メソッドを呼び出すことで、スレッドの終了を待機します。
実行結果
このプログラムを実行すると以下のメッセージが標準出力に吐かれます。Mutexでカウンタを排他しているため、最終的にカウンタの値が200になっていますね。
Thread 1 incremented counter to 1
Thread 2 incremented counter to 2
Thread 2 incremented counter to 3
// 途中は省略
Thread 1 incremented counter to 198
Thread 1 incremented counter to 199
Thread 1 incremented counter to 200
Final counter value: 200
ちなみにMutexによる排他を行わずに実行すると以下のようになります。排他制御をしていないために競合が発生し、正確にカウンタをインクリメントできていないですね。
Thread 1 incremented counter to 1
Thread 2 incremented counter to 1
Thread 2 incremented counter to 2
// 途中は省略
Thread 2 incremented counter to 100
Thread 2 incremented counter to 101
Thread 2 incremented counter to 102
Final counter value: 102
さいごに
マルチスレッドプログラミングでは、適切な排他制御が重要になります。この記事では、排他制御の基本的な概念と代表的な排他手法であるMutexについて紹介しました。