排他制御とは、複数のプロセスやスレッドが、メモリやファイルといった共有リソースに同時にアクセスする際に、データの整合性を保つための仕組みや手法のことを指します。排他制御の目的は、共有リソースに対する競合状態を防ぎ、予期しない動作やデータの破損を避けることです。
排他制御の代表的な手法には以下のようなものがありますが、この記事ではセマフォについて解説し、実際の使い方を紹介したいと思います。
- Mutex
- セマフォ
- リード・ライトロック
- スピンロック
排他制御は、特にマルチスレッドプログラミングや並列処理において重要な概念です。正しく実装されないと、デッドロック(プロセスやスレッドが相互に待ち状態に陥る状態)やスタベーション(あるスレッドが永久にリソースを得られない)などの問題が発生する可能性があります。そのため、排他制御のメカニズムを適切に理解し、正しく活用することが重要です。
C++でマルチスレッドプログラミングをする方法は以下の記事で紹介していますので、マルチスレッドがどういうものかわからない方はご覧ください。
リード・ライトロックとは
リード・ライトロック(Read-Write Lock)とは、共有リソースに対するアクセス制御を行う手法の一つです。
Mutexやセマフォと違う点は、複数のスレッドが同時にリソースを読み取ることは許可しつつ、リソースの書き込み中は他のスレッドがリソースにアクセスできないようにすることで、データの整合性を保ちます。
リード・ライトロックの主な概念
リードロック(共有ロック / Shared Lock)
- 複数のスレッドが同時にリソースを読み取ることを許可します。
- リソースが読み取り専用であれば、他のスレッドが同時に読み取ることができます。
- ライトロックがかかっている場合、リードロックを取得することはできません。
ライトロック(排他ロック / Exclusive Lock)
- 一度に一つのスレッドだけがリソースを読み書きできるようにします。
- ライトロックがかかっている間は、他のスレッドがリードロックもライトロックも取得できません。
- リードロックがかかっている場合、ライトロックを取得することはできません。
リード・ライトロックの利点
- 複数のリードスレッドが同時にリソースにアクセスできるため、読み取り操作が多い場合に性能が向上
実装例
#include <iostream>
#include <shared_mutex>
#include <mutex>
#include <thread>
#include <vector>
#include <chrono>
#include <random>
// 共有リソース
int shared_resource = 0;
const int num_iterations = 10; // 各スレッドが実行する回数
// リード・ライトロック
std::shared_mutex rw_mutex;
// 標準出力の保護
std::mutex cout_mutex;
// ランダムな待機時間を生成するための関数
int random_wait_time(int min_ms, int max_ms) {
static std::mt19937 generator{std::random_device{}()};
std::uniform_int_distribution<int> distribution(min_ms, max_ms);
return distribution(generator);
}
void reader(int id) {
for (int i = 0; i < num_iterations * 2; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(random_wait_time(50, 100))); // ランダムな待機
std::shared_lock<std::shared_mutex> lock(rw_mutex);
// リソースの読み取り
{
std::lock_guard<std::mutex> cout_lock(cout_mutex);
std::cout << "Reader " << id << " reads value: " << shared_resource << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void writer(int id) {
for (int i = 0; i < num_iterations; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(random_wait_time(50, 100))); // ランダムな待機
std::unique_lock<std::shared_mutex> lock(rw_mutex);
// リソースの書き込み
++shared_resource;
{
std::lock_guard<std::mutex> cout_lock(cout_mutex);
std::cout << "Writer " << id << " increments value to: " << shared_resource << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
int main() {
std::vector<std::thread> readers;
std::vector<std::thread> writers;
// 複数のリーダースレッドを生成
for (int i = 0; i < 3; ++i) {
readers.emplace_back(reader, i);
}
// 複数のライタースレッドを生成
for (int i = 0; i < 3; i++) {
writers.emplace_back(writer, i);
}
// スレッドをジョイン
for (auto& reader_thread : readers) {
reader_thread.join();
}
for (auto& writer_thread : writers) {
writer_thread.join();
}
// 最終的な共有リソースの値を出力
std::cout << "Final value of shared_resource: " << shared_resource << std::endl;
return 0;
}
必要なパッケージのインクルード
#include <shared_mutex>
リード・ライトロックを実現するために、shared_mutex
をインクルードします。
共有リソースと定数の定義
int shared_resource = 0;
const int num_iterations = 10;
shared_resource
はスレッド間で共有される整数、num_iterations
は各スレッドが実行するループの回数です。
リード・ライトロックと標準出力保護用のミューテックスの定義
std::shared_mutex rw_mutex;
std::mutex cout_mutex;
rw_mutex
はリード・ライトロック用のミューテックス、cout_mutex
は標準出力の保護用のミューテックスです。
ランダムな待機時間を生成する関数
int random_wait_time(int min_ms, int max_ms) {
static std::mt19937 generator{std::random_device{}()};
std::uniform_int_distribution<int> distribution(min_ms, max_ms);
return distribution(generator);
}
この関数は、指定された範囲内でランダムな待機時間(ミリ秒)を生成します。
リードスレッド関数
void reader(int id) {
for (int i = 0; i < num_iterations * 2; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(random_wait_time(50, 100)));
std::shared_lock<std::shared_mutex> lock(rw_mutex);
{
std::lock_guard<std::mutex> cout_lock(cout_mutex);
std::cout << "Reader " << id << " reads value: " << shared_resource << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
リーダースレッドは、共有リソースをリードロックを取得して読み取ります。その後、標準出力に読み取った値を表示します。
ライトスレッド関数
void writer(int id) {
for (int i = 0; i < num_iterations; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(random_wait_time(50, 100)));
std::unique_lock<std::shared_mutex> lock(rw_mutex);
++shared_resource;
{
std::lock_guard<std::mutex> cout_lock(cout_mutex);
std::cout << "Writer " << id << " increments value to: " << shared_resource << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
ライトスレッドは、共有リソースをライトロックを取得してインクリメントします。その後、標準出力に更新された値を表示します。
メイン関数
int main() {
std::vector<std::thread> readers;
std::vector<std::thread> writers;
for (int i = 0; i < 3; ++i) {
readers.emplace_back(reader, i);
}
for (int i = 0; i < 3; i++) {
writers.emplace_back(writer, i);
}
for (auto& reader_thread : readers) {
reader_thread.join();
}
for (auto& writer_thread : writers) {
writer_thread.join();
}
std::cout << "Final value of shared_resource: " << shared_resource << std::endl;
return 0;
}
メイン関数では、リーダースレッドとライタースレッドをそれぞれ3つずつ生成し、全てのスレッドが完了するのを待機します。最後に、shared_resource
の最終値を表示します。
実行結果
このプログラムを実行すると以下のメッセージが標準出力に吐かれます。リード・ライトロックでカウンタを排他しているため、リード/ライトそれぞれの実行結果に不整合がないことが確認できます。
Reader 1 reads value: 0
Reader 0 reads value: 0
Reader 2 reads value: 0
Writer 0 increments value to: 1
Reader 1 reads value: 1
Reader 2 reads value: 1
Reader 0 reads value: 1
Writer 2 increments value to: 2
Writer 1 increments value to: 3
Reader 2 reads value: 3
Reader 0 reads value: 3
Reader 1 reads value: 3
Writer 0 increments value to: 4
Writer 2 increments value to: 5
Reader 0 reads value: 5
Reader 2 reads value: 5
Reader 1 reads value: 5
Writer 1 increments value to: 6
Writer 0 increments value to: 7
Reader 1 reads value: 7
Reader 2 reads value: 7
Reader 0 reads value: 7
Writer 2 increments value to: 8
Writer 1 increments value to: 9
Reader 1 reads value: 9
Reader 0 reads value: 9
Reader 2 reads value: 9
Writer 0 increments value to: 10
Writer 2 increments value to: 11
Reader 1 reads value: 11
Reader 0 reads value: 11
Reader 2 reads value: 11
Writer 1 increments value to: 12
Writer 0 increments value to: 13
Reader 1 reads value: 13
Reader 0 reads value: 13
Reader 2 reads value: 13
Writer 2 increments value to: 14
Reader 0 reads value: 14
Reader 1 reads value: 14
Reader 2 reads value: 14
Writer 1 increments value to: 15
Writer 0 increments value to: 16
Reader 0 reads value: 16
Reader 2 reads value: 16
Reader 1 reads value: 16
Writer 2 increments value to: 17
Writer 1 increments value to: 18
Reader 1 reads value: 18
Reader 0 reads value: 18
Reader 2 reads value: 18
Writer 0 increments value to: 19
Writer 2 increments value to: 20
Reader 0 reads value: 20
Reader 2 reads value: 20
Reader 1 reads value: 20
Writer 1 increments value to: 21
Writer 0 increments value to: 22
Reader 1 reads value: 22
Reader 0 reads value: 22
Reader 2 reads value: 22
Writer 2 increments value to: 23
Writer 1 increments value to: 24
Reader 2 reads value: 24
Reader 1 reads value: 24
Reader 0 reads value: 24
Writer 0 increments value to: 25
Writer 2 increments value to: 26
Reader 2 reads value: 26
Reader 0 reads value: 26
Reader 1 reads value: 26
Writer 1 increments value to: 27
Writer 0 increments value to: 28
Reader 1 reads value: 28
Reader 0 reads value: 28
Reader 2 reads value: 28
Writer 2 increments value to: 29
Writer 1 increments value to: 30
Reader 1 reads value: 30
Reader 2 reads value: 30
Reader 0 reads value: 30
Reader 0 reads value: 30
Reader 2 reads value: 30
Reader 1 reads value: 30
Reader 2 reads value: 30
Reader 0 reads value: 30
Reader 1 reads value: 30
Reader 2 reads value: 30
Reader 0 reads value: 30
Reader 1 reads value: 30
Final value of shared_resource: 30