排他制御とは、複数のプロセスやスレッドが、メモリやファイルといった共有リソースに同時にアクセスする際に、データの整合性を保つための仕組みや手法のことを指します。排他制御の目的は、共有リソースに対する競合状態を防ぎ、予期しない動作やデータの破損を避けることです。
排他制御の代表的な手法には以下のようなものがありますが、この記事ではスピンロックについて解説し、実際の使い方を紹介したいと思います。
- Mutex
- セマフォ
- リード・ライトロック
- スピンロック
排他制御は、特にマルチスレッドプログラミングや並列処理において重要な概念です。正しく実装されないと、デッドロック(プロセスやスレッドが相互に待ち状態に陥る状態)やスタベーション(あるスレッドが永久にリソースを得られない)などの問題が発生する可能性があります。そのため、排他制御のメカニズムを適切に理解し、正しく活用することが重要です。
C++でマルチスレッドプログラミングをする方法は以下の記事で紹介していますので、マルチスレッドがどういうものかわからない方はご覧ください。
スピンロックとは
概要
スピンロックは、あるスレッドがロックを取得するために、ロックが解放されるまで「スピン(ループしながら待機)」するという排他方法です。具体的には、スレッドはループ内でロックの状態をチェックし続け、ロックが解放されるとすぐにロックを取得します。
利点
- 高速なコンテキストスイッチが不要
-
スピンロックは短期間でのロック取得が予想される場合に有効で、スレッドをブロックしないため、オーバーヘッドが少なくなります。
- シンプルな実装
-
実装が比較的単純で、ハードウェアのアトミック操作を利用してロックを管理します。
欠点
- CPUリソースの無駄
-
スピンロックはロックが解放されるまでループし続けるため、CPUリソースを無駄に消費します。特に、長時間ロックが取得できない場合に問題となります。
- スケーラビリティの問題
-
マルチプロセッサシステムで大量のスレッドがスピンロックを待機する場合、パフォーマンスの低下が発生する可能性があります。
実装例
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
class Spinlock {
public:
Spinlock() : flag(ATOMIC_FLAG_INIT) {}
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) {
// スピン: ロックが解放されるまで待機
}
}
void unlock() {
flag.clear(std::memory_order_release);
}
private:
std::atomic_flag flag;
};
Spinlock spinlock;
int counter = 0;
void incrementCounter(int iterations) {
for (int i = 0; i < iterations; ++i) {
spinlock.lock();
++counter;
spinlock.unlock();
}
}
int main() {
const int numThreads = 10;
const int iterations = 1000;
std::vector<std::thread> threads;
// スレッドを起動
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back(incrementCounter, iterations);
}
// スレッドの終了を待つ
for (auto& thread : threads) {
thread.join();
}
std::cout << "Counter: " << counter << std::endl;
return 0;
}
Spinlockクラスの定義
class Spinlock {
public:
Spinlock() : flag(ATOMIC_FLAG_INIT) {}
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) {
// スピン: ロックが解放されるまで待機
}
}
void unlock() {
flag.clear(std::memory_order_release);
}
private:
std::atomic_flag flag;
};
Spinlock() : flag(ATOMIC_FLAG_INIT) {}
: ロックの状態を管理するアトミックフラグを定義し、コンストラクタで初期化します。void lock()
: ロックを取得するためのメソッド。flag.test_and_set
を使用してロックを試み、ロックが取得されるまでループします。void unlock()
: ロックを解放するためのメソッド。flag.clear
を使用してロックを解放します。
カウンタをインクリメントする関数
void incrementCounter(int iterations) {
for (int i = 0; i < iterations; ++i) {
spinlock.lock();
++counter;
spinlock.unlock();
}
}
incrementCounter
関数は、指定された回数だけカウンタをインクリメントします。spinlock.lock()
でロックを取得し、counter
をインクリメントした後、spinlock.unlock()
でロックを解放します。
メイン関数
int main() {
const int numThreads = 10;
const int iterations = 1000;
std::vector<std::thread> threads;
// スレッドを起動
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back(incrementCounter, iterations);
}
// スレッドの終了を待つ
for (auto& thread : threads) {
thread.join();
}
std::cout << "Counter: " << counter << std::endl;
return 0;
}
スレッドを起動してincrementCounter
関数を実行し、最終的なカウンタの値を出力します。
実行結果
このプログラムを実行すると以下のメッセージが標準出力に吐かれます。スピンロックでカウンタを排他しているため、最終的にカウンタの値が10000になっています。
Counter: 10000
スピンロックによる排他を行わずに実行すると以下のようになります。排他制御をしていないために競合が発生し、正確にカウンタをインクリメントできていないことが分かります。
Counter: 9997