【C++】std::shared_mutex の使い方 | 読み取りのパフォーマンスを向上させる方法

目次

はじめに

マルチスレッドプログラミングにおいて、共有リソースへのアクセスを std::mutex で保護すると、読み取りと書き込みの両方が排他的になります。しかし、「書き込みはほとんど発生せず、大半が読み取り」という状況では、本来同時に実行できるはずの読み取り処理までもが、一つずつ順番待ちすることになり、パフォーマンスが著しく低下します。

この「Reader-Writer問題」を解決するのが、C++17で導入された std::shared_mutex です。shared_mutex は、2種類のロックを提供します。

  1. 共有ロック (Shared Lock): 複数のスレッドが同時に取得可能。読み取り処理で使います。
  2. 排他ロック (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_mutexlock() を呼び出します。
  • このロックが保持されている間、他のどのスレッドも、排他ロックも共有ロックも新たに取得することはできません。

2. 読み手と共有ロック (shared_lock)

shared_lock<shared_mutex> lock(prompter.mtx); 読み取り処理では、共有ロックを使います。shared_mutex に対して共有ロックを取得する場合、RAIIラッパーとして std::shared_lock を使います。(shared_lock はC++14で導入されました)

  • shared_lock のコンストラクタは、shared_mutexlock_shared() を呼び出します。
  • あるスレッドが共有ロックを保持している間、他のスレッドは排他ロックを取得することはできませんが、別の共有ロックは取得できます。これにより、複数の読み手スレッドが同時に prompter.script を読み取ることが可能になります。

まとめ

今回は、読み取り処理が頻繁に発生する状況でのパフォーマンスを向上させる std::shared_mutex について解説しました。

  • std::shared_mutex は、共有ロック排他ロックの2種類を提供する。
  • 読み取り処理では shared_lock を使って共有ロックを取得し、読み取りの並列実行を可能にする。
  • 書き込み処理では unique_lock を使って排他ロックを取得し、データの安全な更新を保証する。

std::mutex による単純な排他制御では性能が出ない、という場面に遭遇したら、std::shared_mutex の導入を検討してみましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次