はじめに
マルチスレッドプログラミングにおいて、共有リソースへのアクセスを std::mutex
で保護すると、読み取りと書き込みの両方が排他的になります。しかし、「書き込みはほとんど発生せず、大半が読み取り」という状況では、本来同時に実行できるはずの読み取り処理までもが、一つずつ順番待ちすることになり、パフォーマンスが著しく低下します。
この「Reader-Writer問題」を解決するのが、C++17で導入された std::shared_mutex
です。shared_mutex
は、2種類のロックを提供します。
- 共有ロック (Shared Lock): 複数のスレッドが同時に取得可能。読み取り処理で使います。
- 排他ロック (Exclusive Lock): 一度に一つのスレッドしか取得できない。書き込み処理で使います。
この記事では、shared_mutex
を使って、読み取り処理の並列性を高め、パフォーマンスを向上させる方法を解説します。
shared_mutex
を使ったリーダー/ライターロック
このコードは、共有のTeleprompter
(テレビのスピーチ原稿)を、一人の「書き手(writer
)」が更新し、複数の「読み手(reader
)」が同時に読み取る、というシナリオをシミュレートします。
完成コード
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <shared_mutex> // shared_mutex を使うために必要
#include <vector>
using namespace std;
// 共有リソース
struct Teleprompter {
string script = "スピーチを開始します...";
mutable std::shared_mutex mtx; // 読み取り処理(const)からもロックできるようmutableを付ける
};
// 書き手スレッドの処理
void writer(Teleprompter& prompter) {
// 1. 排他ロックを取得 (RAIIラッパー: unique_lock)
unique_lock<shared_mutex> lock(prompter.mtx);
cout << "[書き手] が原稿を更新中です..." << endl;
prompter.script = "これが新しいスピーチ原稿です。";
this_thread::sleep_for(chrono::seconds(1));
cout << "[書き手] が更新を完了しました。" << endl;
// lockがスコープを抜ける際に自動でアンロック
}
// 読み手スレッドの処理
void reader(const Teleprompter& prompter, int id) {
// 2. 共有ロックを取得 (RAIIラッパー: shared_lock)
shared_lock<shared_mutex> lock(prompter.mtx);
cout << "[読み手 " << id << "] が原稿を読み取ります: 「" << prompter.script << "」" << endl;
this_thread::sleep_for(chrono::milliseconds(500));
// lockがスコープを抜ける際に自動でアンロック
}
int main() {
Teleprompter prompter;
// 最初に3人の読み手を起動
vector<thread> readers;
for (int i = 0; i < 3; ++i) {
readers.emplace_back(reader, ref(prompter), i + 1);
}
this_thread::sleep_for(chrono::milliseconds(100));
// 次に書き手を起動
thread writer_thread(writer, ref(prompter));
for (auto& th : readers) {
th.join();
}
writer_thread.join();
return 0;
}
コードの解説
std::shared_mutex
std::mutex
と同様に、共有リソースを保護するためのミューテックスですが、ロックの方法が2種類あります。
- 排他ロック:
lock()
/unlock()
- 共有ロック:
lock_shared()
/unlock_shared()
1. 書き手と排他ロック (unique_lock
)
unique_lock<shared_mutex> lock(prompter.mtx);
書き込み処理は、リソースを完全に排他制御する必要があります。shared_mutex
に対して排他ロックを取得する場合、RAIIラッパーとして std::unique_lock
を使います。
unique_lock
のコンストラクタは、shared_mutex
のlock()
を呼び出します。- このロックが保持されている間、他のどのスレッドも、排他ロックも共有ロックも新たに取得することはできません。
2. 読み手と共有ロック (shared_lock
)
shared_lock<shared_mutex> lock(prompter.mtx);
読み取り処理では、共有ロックを使います。shared_mutex
に対して共有ロックを取得する場合、RAIIラッパーとして std::shared_lock
を使います。(shared_lock
はC++14で導入されました)
shared_lock
のコンストラクタは、shared_mutex
のlock_shared()
を呼び出します。- あるスレッドが共有ロックを保持している間、他のスレッドは排他ロックを取得することはできませんが、別の共有ロックは取得できます。これにより、複数の読み手スレッドが同時に
prompter.script
を読み取ることが可能になります。
まとめ
今回は、読み取り処理が頻繁に発生する状況でのパフォーマンスを向上させる std::shared_mutex
について解説しました。
std::shared_mutex
は、共有ロックと排他ロックの2種類を提供する。- 読み取り処理では
shared_lock
を使って共有ロックを取得し、読み取りの並列実行を可能にする。 - 書き込み処理では
unique_lock
を使って排他ロックを取得し、データの安全な更新を保証する。
std::mutex
による単純な排他制御では性能が出ない、という場面に遭遇したら、std::shared_mutex
の導入を検討してみましょう。