排他制御とは、複数のプロセスやスレッドが、メモリやファイルといった共有リソースに同時にアクセスする際に、データの整合性を保つための仕組みや手法のことを指します。排他制御の目的は、共有リソースに対する競合状態を防ぎ、予期しない動作やデータの破損を避けることです。
排他制御の代表的な手法には以下のようなものがありますが、この記事ではセマフォについて解説し、実際の使い方を紹介したいと思います。
- Mutex
- セマフォ
- リード・ライトロック
- スピンロック
排他制御は、特にマルチスレッドプログラミングや並列処理において重要な概念です。正しく実装されないと、デッドロック(プロセスやスレッドが相互に待ち状態に陥る状態)やスタベーション(あるスレッドが永久にリソースを得られない)などの問題が発生する可能性があります。そのため、排他制御のメカニズムを適切に理解し、正しく活用することが重要です。
C++でマルチスレッドプログラミングをする方法は以下の記事で紹介していますので、マルチスレッドがどういうものかわからない方はご覧ください。
セマフォとは
セマフォ(Semaphore)は、スレッドやプロセス間の同期を行うためのツールです。
セマフォはカウンタであり、資源の数を管理するために使われます。セマフォにはバイナリセマフォとカウントセマフォの2種類があります。
- バイナリセマフォ
-
バイナリセマフォは、カウンタが0または1の値を取るセマフォです。バイナリセマフォは、ミューテックスのように動作し、あるスレッドがリソースを使用している間、他のスレッドがそのリソースにアクセスするのを防ぎます。
- カウントセマフォ
-
カウントセマフォは、任意の非負整数値を取るセマフォです。カウントセマフォは、特定の数の同時リソースアクセスを許可するために使用されます。例えば、リソースの最大数が5の場合、5つのスレッドが同時にリソースにアクセスでき、それ以上のスレッドはリソースが解放されるまで待機します。
C++でセマフォを使用するには、C++11以降で提供されている標準ライブラリの機能を利用するか、Boostライブラリを使用する方法があります。
この記事では、それぞれの実装方法を紹介します。
C++の標準ライブラリを使用した実装例
C++11標準ライブラリにはセマフォの直接的なサポートはありませんが、std::condition_variable
とstd::mutex
を組み合わせることでセマフォを実装することができます。
以下のコードでは、2つのスレッドが共有のカウンタをインクリメントします。、2つのスレッドが同時にカウンタをインクリメントするのを防ぐためにセマフォを使用します。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
int counter = 0; // 共有カウンタ
class Semaphore {
public:
Semaphore(int count = 0) : count(count) {}
void notify() {
std::unique_lock<std::mutex> lock(mtx);
++count;
cv.notify_one();
}
void wait() {
std::unique_lock<std::mutex> lock(mtx);
while(count == 0) {
cv.wait(lock);
}
--count;
}
private:
std::mutex mtx;
std::condition_variable cv;
int count;
};
Semaphore sem(1); // セマフォを1で初期化
void incrementCounter(int id) {
for (int i = 0; i < 100; ++i) {
sem.wait();
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;
sem.notify();
}
}
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;
}
セマフォクラスの定義
class Semaphore {
public:
Semaphore(int count = 0) : count(count) {}
void notify() {
std::unique_lock<std::mutex> lock(mtx);
++count;
cv.notify_one();
}
void wait() {
std::unique_lock<std::mutex> lock(mtx);
while(count == 0) {
cv.wait(lock);
}
--count;
}
private:
std::mutex mtx;
std::condition_variable cv;
int count;
};
Semaphore
クラスは、スレッド間の同期を提供するためのセマフォを実装しています。
Semaphore
クラスは以下の3つのメンバ変数を持ちます。
std::mutex mtx
:排他制御のためのMutexで、セマフォカウンタへのアクセスを保護します。std::condition_variable cv
:条件変数。セマフォの状態変化を待機するために使用されます。int count
:セマフォカウンタで、初期値はコンストラクタで設定されます。
また、以下の2つのメンバ関数を持ちます。
notify
:Mutexのロックしカウンタをインクリメントします。そして、条件変数cv
を通知して、待機しているスレッドにセマフォのカウンタがインクリメントされたことを知らせます。wait
:Mutexをロックし、カウンタが0の場合は条件変数cv
を使用して待機します。カウンタが0でなくなったら、カウンタをデクリメントします。
セマフォの初期化
Semaphore sem(1); // セマフォを1で初期化
セマフォsem
を初期カウンタ1で初期化します。これにより、1つのスレッドがセマフォを取得できます。
スレッド関数の定義
void incrementCounter(int id) {
for (int i = 0; i < 100; ++i) {
sem.wait();
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;
sem.notify();
}
}
スレッドが実行する関数incrementCounter
は、セマフォを使用して共有カウンタを100回インクリメントします。
各ループで、セマフォをwait
で取得し、カウンタを読み込み、1マイクロ秒の遅延を追加し、カウンタをインクリメントし、セマフォをnotify
で解放します。
main関数の定義
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;
}
main
関数では、2つのスレッドを作成し、それぞれincrementCounter
関数を実行します。
Boostライブラリを使用した実装例
Boostライブラリを使用することで、直接セマフォを使用することができます。
まず、以下のコマンドでBoostライブラリをインストールします。
sudo apt install libboost-all-dev
以下のコードも先ほどと同様に、2つのスレッドが共有のカウンタをインクリメントします。、2つのスレッドが同時にカウンタをインクリメントするのを防ぐためにセマフォを使用します。
#include <iostream>
#include <thread>
#include <boost/interprocess/sync/interprocess_semaphore.hpp>
int counter = 0; // 共有カウンタ
class Semaphore {
public:
Semaphore(int count = 0) : sem(count) {}
void notify() {
sem.post();
}
void wait() {
sem.wait();
}
private:
boost::interprocess::interprocess_semaphore sem;
};
Semaphore sem(1); // セマフォを1で初期化
void incrementCounter(int id) {
for (int i = 0; i < 100; ++i) {
sem.wait();
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;
sem.notify();
}
}
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;
}
基本的なつくりは標準ライブラリを使用した時と同じですが、Boostライブラリを使用すれば直接セマフォを使用できるため、実装がより簡単になります。
実行結果
このプログラムを実行すると以下のメッセージが標準出力に吐かれます。セマフォでカウンタを排他しているため、最終的にカウンタの値が200になっています。
Thread 1 incremented counter to 1
Thread 1 incremented counter to 2
Thread 1 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
セマフォによる排他を行わずに実行すると以下のようになります。排他制御をしていないために競合が発生し、正確にカウンタをインクリメントできていないことが分かります。
Thread 2 incremented counter to 1
Thread 1 incremented counter to 1
Thread 1 incremented counter to 2
// 途中は省略
Thread 2 incremented counter to 104
Thread 2 incremented counter to 105
Thread 2 incremented counter to 106
Final counter value: 106
さいごに
マルチスレッドプログラミングでは、適切な排他制御が重要になります。この記事では、排他制御の基本的な概念と代表的な排他手法であるセマフォについて紹介しました。
セマフォと同じく代表的な排他制御手法であるMutexについても以下の記事で解説していますのでご覧ください。