C++のstd::shared_timed_mutexで実現する複数読者単一著者ロックの使い方

C++のマルチスレッドプログラミングでは、複数のスレッドが同時にデータにアクセスする際にデータ競合を防ぐための同期機構が重要です。特に、読み取り専用のスレッドが多数存在し、書き込みを行うスレッドが少ない場合、効率的な同期機構が求められます。ここで登場するのがstd::shared_timed_mutexです。本記事では、C++標準ライブラリに含まれるstd::shared_timed_mutexを使って、複数のスレッドによる読み取りと単一スレッドによる書き込みを効率よく行う方法について詳しく解説します。具体的な実装例や応用例を交えながら、この同期機構の利点と使い方を学びましょう。

目次

std::shared_timed_mutexとは

std::shared_timed_mutexは、C++17で導入された同期プリミティブの一つで、複数のスレッドが同時に読み取り操作を行いながら、書き込み操作を行うスレッドが単一で存在することを可能にするロック機構です。このクラスは、従来のstd::mutexstd::shared_mutexの機能に加えて、タイムアウト付きのロック機能を提供します。

基本概念

std::shared_timed_mutexは、以下の2種類のロックを提供します。

  1. 共有ロック(shared lock): 複数のスレッドが同時に読み取り操作を行うことができます。
  2. 排他ロック(exclusive lock): 一つのスレッドだけが書き込み操作を行うことができます。他のスレッドは、読み取りも書き込みもできません。

この仕組みにより、読み取りが多く、書き込みが少ない状況でパフォーマンスの向上が期待できます。また、タイムアウト付きのロック機能により、ロックの取得を待つ時間を制限することができます。これにより、デッドロックのリスクを軽減することができます。

使い方の概要

以下に、std::shared_timed_mutexの基本的な使い方の概要を示します。

#include <shared_mutex>
#include <thread>
#include <chrono>
#include <iostream>

std::shared_timed_mutex sharedMutex;

void reader() {
    std::shared_lock<std::shared_timed_mutex> lock(sharedMutex);
    // 読み取り操作
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "Reader thread completed.\n";
}

void writer() {
    std::unique_lock<std::shared_timed_mutex> lock(sharedMutex);
    // 書き込み操作
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "Writer thread completed.\n";
}

int main() {
    std::thread t1(reader);
    std::thread t2(writer);
    t1.join();
    t2.join();
    return 0;
}

上記の例では、reader関数が共有ロックを取得し、writer関数が排他ロックを取得しています。このようにして、複数の読み取りスレッドと単一の書き込みスレッドが安全にデータにアクセスできるようにします。

使い方の基本

std::shared_timed_mutexを使うことで、複数のスレッドが同時にデータを読み取る一方で、単一のスレッドだけがデータを書き込むことができます。このセクションでは、基本的な使い方を具体的に解説します。

共有ロック(shared lock)

共有ロックは複数のスレッドが同時に読み取り操作を行う場合に使用します。以下のコード例では、複数の読み取りスレッドが同時にデータを読み取る方法を示しています。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void reader(int id) {
    std::shared_lock<std::shared_timed_mutex> lock(sharedMutex);
    std::cout << "Reader " << id << " reads sharedData: " << sharedData << std::endl;
}

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    for (auto& reader : readers) {
        reader.join();
    }
    return 0;
}

この例では、5つの読み取りスレッドが同時にsharedDataの値を読み取ります。std::shared_lockを使用して共有ロックを取得し、他のスレッドと並行して読み取りを行います。

排他ロック(exclusive lock)

排他ロックは、データを書き込む場合に使用します。以下のコード例では、単一の書き込みスレッドがデータを書き込む方法を示しています。

#include <iostream>
#include <shared_mutex>
#include <thread>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void writer() {
    std::unique_lock<std::shared_timed_mutex> lock(sharedMutex);
    sharedData++;
    std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
}

int main() {
    std::thread t1(writer);
    t1.join();
    return 0;
}

この例では、書き込みスレッドがsharedDataの値をインクリメントします。std::unique_lockを使用して排他ロックを取得し、他のスレッドがデータにアクセスできないようにします。

タイムアウト付きロック

std::shared_timed_mutexはタイムアウト付きのロック機能も提供します。これにより、ロックの取得を一定時間待ち、取得できない場合は処理を中断することができます。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <chrono>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void try_writer() {
    if (sharedMutex.try_lock_for(std::chrono::milliseconds(100))) {
        sharedData++;
        std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
        sharedMutex.unlock();
    } else {
        std::cout << "Writer could not acquire lock\n";
    }
}

int main() {
    std::thread t1(try_writer);
    t1.join();
    return 0;
}

この例では、書き込みスレッドがタイムアウト付きでロックを試みます。100ミリ秒以内にロックを取得できなかった場合、書き込み操作をスキップします。

これらの基本的な使い方を理解することで、std::shared_timed_mutexを効果的に使用して、複数のスレッドがデータにアクセスする状況を管理することができます。

複数読者単一著者ロックの実装例

std::shared_timed_mutexを使用することで、複数のスレッドが同時に読み取りを行い、単一のスレッドが書き込みを行う同期機構を効率的に実装できます。このセクションでは、具体的な実装例を示しながら解説します。

実装例の全体像

以下に示すコードは、複数の読み取りスレッドと単一の書き込みスレッドが共有データにアクセスするシナリオを実装したものです。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void reader(int id) {
    while (true) {
        std::shared_lock<std::shared_timed_mutex> lock(sharedMutex);
        std::cout << "Reader " << id << " reads sharedData: " << sharedData << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void writer() {
    while (true) {
        std::unique_lock<std::shared_timed_mutex> lock(sharedMutex);
        sharedData++;
        std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
}

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    std::thread writerThread(writer);

    for (auto& reader : readers) {
        reader.join();
    }
    writerThread.join();

    return 0;
}

コード解説

このコードでは、5つの読み取りスレッドと1つの書き込みスレッドが同時に動作しています。それぞれの関数について詳しく見ていきましょう。

読み取りスレッド(reader)

読み取りスレッドは、std::shared_lockを使用して共有ロックを取得し、sharedDataの値を読み取ります。

void reader(int id) {
    while (true) {
        std::shared_lock<std::shared_timed_mutex> lock(sharedMutex);
        std::cout << "Reader " << id << " reads sharedData: " << sharedData << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

各スレッドは無限ループで動作し、定期的にsharedDataの値を読み取ってコンソールに出力します。std::shared_lockにより、他の読み取りスレッドと並行して動作することが可能です。

書き込みスレッド(writer)

書き込みスレッドは、std::unique_lockを使用して排他ロックを取得し、sharedDataの値をインクリメントします。

void writer() {
    while (true) {
        std::unique_lock<std::shared_timed_mutex> lock(sharedMutex);
        sharedData++;
        std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
}

書き込み操作は他のスレッドが読み取りや書き込みを行っていない間にのみ実行されます。無限ループで動作し、定期的にsharedDataの値を更新します。

メイン関数

メイン関数では、5つの読み取りスレッドと1つの書き込みスレッドを生成し、それぞれを並行して実行します。

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    std::thread writerThread(writer);

    for (auto& reader : readers) {
        reader.join();
    }
    writerThread.join();

    return 0;
}

ここでは、std::vectorを使って複数の読み取りスレッドを管理し、それぞれのスレッドを生成します。各スレッドが終了するまで待機するために、joinを使用しています。

この実装例を通じて、std::shared_timed_mutexを用いた複数読者単一著者ロックの基本的な使い方が理解できるでしょう。この技術を応用することで、効率的なマルチスレッドプログラミングが可能になります。

読み取りロックの取得方法

読み取りロックを取得するためには、std::shared_lockを使用します。これにより、複数のスレッドが同時にデータを読み取ることが可能になります。このセクションでは、読み取りロックの取得方法とその注意点を解説します。

基本的な読み取りロックの取得方法

std::shared_lockを使うことで、読み取りロックを簡単に取得できます。以下の例では、std::shared_lockを使用して共有データを読み取る方法を示します。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void reader(int id) {
    // 共有ロックの取得
    std::shared_lock<std::shared_timed_mutex> lock(sharedMutex);
    std::cout << "Reader " << id << " reads sharedData: " << sharedData << std::endl;
}

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    for (auto& reader : readers) {
        reader.join();
    }
    return 0;
}

上記の例では、各読み取りスレッドがsharedMutexの共有ロックを取得し、sharedDataの値を読み取っています。std::shared_lockは他の読み取りスレッドと並行して動作するため、データの一貫性が保たれます。

再帰的な読み取りロックの取得

std::shared_timed_mutexは再帰的なロックをサポートしていません。そのため、同一スレッドが複数回読み取りロックを取得しようとするとデッドロックが発生する可能性があります。以下の例は、再帰的なロックの取得を避ける方法を示します。

#include <iostream>
#include <shared_mutex>
#include <thread>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void recursiveReader(int depth) {
    if (depth == 0) return;
    std::shared_lock<std::shared_timed_mutex> lock(sharedMutex);
    std::cout << "Reading at depth " << depth << ": " << sharedData << std::endl;
    recursiveReader(depth - 1);
}

int main() {
    std::thread readerThread(recursiveReader, 3);
    readerThread.join();
    return 0;
}

この例では、recursiveReader関数が再帰的に呼び出されますが、各レベルで新たなロックを取得するため、再帰的なロックを避けることができます。

注意点とベストプラクティス

読み取りロックを使用する際には、以下の点に注意してください。

  1. ロックの保持時間を最小限にする:
    ロックを取得したら、必要な読み取り操作を速やかに行い、ロックを解除するように心がけます。これにより、他のスレッドがスムーズにロックを取得できるようになります。
  2. デッドロックを避ける:
    同一スレッド内で再帰的にロックを取得しないように設計します。再帰的なロックはデッドロックの原因となるため、注意が必要です。
  3. 適切なロックの種類を選択する:
    読み取り専用の操作には共有ロックを、書き込み操作には排他ロックを使用します。誤ったロックの種類を使用すると、データの一貫性が保たれなくなります。

以上のポイントを押さえることで、std::shared_timed_mutexを効果的に活用し、マルチスレッド環境での安全な読み取り操作が実現できます。

書き込みロックの取得方法

書き込みロックを取得するためには、std::unique_lockを使用します。これにより、単一のスレッドだけがデータに書き込みを行うことが可能になり、他のスレッドが同時に読み取りや書き込みを行うのを防ぎます。このセクションでは、書き込みロックの取得方法とその注意点を解説します。

基本的な書き込みロックの取得方法

std::unique_lockを使うことで、書き込みロックを簡単に取得できます。以下の例では、std::unique_lockを使用して共有データに書き込みを行う方法を示します。

#include <iostream>
#include <shared_mutex>
#include <thread>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void writer() {
    // 排他ロックの取得
    std::unique_lock<std::shared_timed_mutex> lock(sharedMutex);
    sharedData++;
    std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
}

int main() {
    std::thread writerThread(writer);
    writerThread.join();
    return 0;
}

上記の例では、書き込みスレッドがsharedMutexの排他ロックを取得し、sharedDataの値をインクリメントしています。std::unique_lockにより、他のスレッドがデータにアクセスするのを防ぎ、データの一貫性を保ちます。

タイムアウト付き書き込みロック

std::shared_timed_mutexはタイムアウト付きの書き込みロック機能も提供します。これにより、ロックの取得を一定時間待ち、取得できない場合は処理を中断することができます。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <chrono>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void try_writer() {
    if (sharedMutex.try_lock_for(std::chrono::milliseconds(100))) {
        sharedData++;
        std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
        sharedMutex.unlock();
    } else {
        std::cout << "Writer could not acquire lock\n";
    }
}

int main() {
    std::thread writerThread(try_writer);
    writerThread.join();
    return 0;
}

この例では、書き込みスレッドがタイムアウト付きでロックを試みます。100ミリ秒以内にロックを取得できなかった場合、書き込み操作をスキップします。

注意点とベストプラクティス

書き込みロックを使用する際には、以下の点に注意してください。

  1. ロックの保持時間を最小限にする:
    ロックを取得したら、必要な書き込み操作を速やかに行い、ロックを解除するように心がけます。これにより、他のスレッドがスムーズにロックを取得できるようになります。
  2. デッドロックを避ける:
    他のリソースと一緒にロックを取得する場合、取得する順序に注意を払い、デッドロックが発生しないように設計します。
  3. 適切なロックの種類を選択する:
    書き込み操作には排他ロックを、読み取り専用の操作には共有ロックを使用します。誤ったロックの種類を使用すると、データの一貫性が保たれなくなります。
  4. タイムアウトを活用する:
    タイムアウト付きロックを活用することで、ロックの取得に失敗した場合の処理を柔軟に行うことができます。これにより、デッドロックのリスクを軽減できます。

以上のポイントを押さえることで、std::shared_timed_mutexを効果的に活用し、マルチスレッド環境での安全な書き込み操作が実現できます。

タイムアウト付きロック

std::shared_timed_mutexは、タイムアウト付きのロック機能を提供します。これにより、ロックの取得を一定時間待ち、取得できない場合は処理を中断することができます。このセクションでは、タイムアウト付きロックの使用方法とその利点を解説します。

タイムアウト付き共有ロックの取得方法

タイムアウト付き共有ロックを取得するためには、try_lock_fortry_lock_untilメソッドを使用します。以下の例では、try_lock_forを使用して共有データを読み取る方法を示します。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <chrono>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void reader(int id) {
    if (sharedMutex.try_lock_shared_for(std::chrono::milliseconds(100))) {
        std::cout << "Reader " << id << " reads sharedData: " << sharedData << std::endl;
        sharedMutex.unlock_shared();
    } else {
        std::cout << "Reader " << id << " could not acquire lock\n";
    }
}

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    for (auto& reader : readers) {
        reader.join();
    }
    return 0;
}

上記の例では、各読み取りスレッドが100ミリ秒以内に共有ロックを取得しようと試みます。取得に成功した場合、sharedDataの値を読み取り、ロックを解除します。取得に失敗した場合、メッセージを表示します。

タイムアウト付き排他ロックの取得方法

タイムアウト付き排他ロックを取得するためにも、同様にtry_lock_fortry_lock_untilメソッドを使用します。以下の例では、try_lock_forを使用して共有データに書き込みを行う方法を示します。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <chrono>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void writer() {
    if (sharedMutex.try_lock_for(std::chrono::milliseconds(100))) {
        sharedData++;
        std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
        sharedMutex.unlock();
    } else {
        std::cout << "Writer could not acquire lock\n";
    }
}

int main() {
    std::thread writerThread(writer);
    writerThread.join();
    return 0;
}

この例では、書き込みスレッドが100ミリ秒以内に排他ロックを取得しようと試みます。取得に成功した場合、sharedDataの値をインクリメントし、ロックを解除します。取得に失敗した場合、メッセージを表示します。

タイムアウト付きロックの利点

タイムアウト付きロックを使用することで、以下の利点があります。

  1. デッドロックの回避:
    ロックの取得に失敗した場合に処理を中断できるため、デッドロックのリスクを軽減できます。
  2. 応答性の向上:
    長時間ロックの取得を待つことなく、別の処理に切り替えることができるため、プログラム全体の応答性が向上します。
  3. 柔軟なエラーハンドリング:
    ロックの取得に失敗した場合の処理を柔軟に行うことができるため、より堅牢なプログラムを実現できます。

以上のように、std::shared_timed_mutexを用いたタイムアウト付きロックは、マルチスレッドプログラミングにおいて非常に有用です。適切に使用することで、プログラムのパフォーマンスと信頼性を向上させることができます。

パフォーマンスの考慮

std::shared_timed_mutexを使用する際には、パフォーマンスの考慮が重要です。適切に使用しないと、意図した通りのパフォーマンス向上が得られないことがあります。このセクションでは、パフォーマンスに関する注意点と最適化のためのベストプラクティスを解説します。

読み取りと書き込みのバランス

std::shared_timed_mutexは、読み取りが多く、書き込みが少ない場合に特に有効です。読み取り操作が頻繁に行われ、書き込みが少ない場合は、共有ロックを使用することでパフォーマンスが向上します。しかし、書き込みが頻繁に発生すると、読み取りスレッドがロックを待つ時間が増加し、パフォーマンスが低下する可能性があります。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void reader(int id) {
    for (int i = 0; i < 10; ++i) {
        std::shared_lock<std::shared_timed_mutex> lock(sharedMutex);
        std::cout << "Reader " << id << " reads sharedData: " << sharedData << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

void writer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::shared_timed_mutex> lock(sharedMutex);
        sharedData++;
        std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    std::thread writerThread(writer);

    for (auto& reader : readers) {
        reader.join();
    }
    writerThread.join();

    return 0;
}

この例では、5つの読み取りスレッドが存在し、1つの書き込みスレッドが定期的にsharedDataの値を更新します。書き込みが頻繁に発生しないように、書き込み操作の間に一定の遅延を設けています。

ロックの保持時間を最小限にする

ロックの保持時間が長いと、他のスレッドがロックを取得するのを待つ時間が増加します。ロックの保持時間を最小限にするために、以下の点に注意してください。

  • 必要な操作だけをロック内で行い、余分な処理を避ける。
  • ループ内でロックを取得する場合、ループ全体ではなく、必要な部分だけをロックする。
void optimizedReader(int id) {
    for (int i = 0; i < 10; ++i) {
        {
            std::shared_lock<std::shared_timed_mutex> lock(sharedMutex);
            std::cout << "Reader " << id << " reads sharedData: " << sharedData << std::endl;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

void optimizedWriter() {
    for (int i = 0; i < 10; ++i) {
        {
            std::unique_lock<std::shared_timed_mutex> lock(sharedMutex);
            sharedData++;
            std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

上記の例では、ロックの取得と解除をブロック内に閉じ込めることで、ロックの保持時間を短縮しています。

適切なデータ構造の選択

データ構造によっては、std::shared_timed_mutex以外の同期機構が適している場合があります。例えば、読み取り専用のデータ構造や、特定の条件下でロックフリーのデータ構造を使用することで、パフォーマンスを向上させることができます。

  • 読み取り専用データ構造: 読み取り専用のデータを扱う場合、同期機構を使用せずにデータにアクセスできます。
  • ロックフリーのデータ構造: 特定の条件下で、ロックフリーのデータ構造を使用することで、スレッド間の競合を回避できます。
#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> atomicData{0};

void atomicWriter() {
    for (int i = 0; i < 10; ++i) {
        atomicData++;
        std::cout << "Atomic writer increments atomicData to: " << atomicData << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

int main() {
    std::thread writerThread(atomicWriter);
    writerThread.join();
    return 0;
}

この例では、std::atomicを使用することで、ロックを使用せずにデータを安全に更新しています。

以上のポイントを押さえることで、std::shared_timed_mutexを効果的に使用し、マルチスレッド環境でのパフォーマンスを最大化することができます。

応用例

std::shared_timed_mutexを利用することで、複雑なマルチスレッド環境でも効率的にデータの同期を取ることができます。このセクションでは、より高度な応用例をいくつか紹介します。

プロデューサー-コンシューマーモデル

プロデューサー-コンシューマーモデルは、典型的なマルチスレッドの問題です。ここでは、プロデューサースレッドがデータを生成し、コンシューマースレッドがデータを消費します。std::shared_timed_mutexを使用してデータの一貫性を保ちながらこのモデルを実装します。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
#include <queue>

std::shared_timed_mutex sharedMutex;
std::queue<int> dataQueue;

void producer(int id) {
    for (int i = 0; i < 10; ++i) {
        {
            std::unique_lock<std::shared_timed_mutex> lock(sharedMutex);
            dataQueue.push(i + id * 10);
            std::cout << "Producer " << id << " produced: " << i + id * 10 << std::endl;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer(int id) {
    while (true) {
        {
            std::shared_lock<std::shared_timed_mutex> lock(sharedMutex);
            if (!dataQueue.empty()) {
                int value = dataQueue.front();
                dataQueue.pop();
                std::cout << "Consumer " << id << " consumed: " << value << std::endl;
            } else {
                break;
            }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

int main() {
    std::vector<std::thread> producers;
    std::vector<std::thread> consumers;

    for (int i = 0; i < 3; ++i) {
        producers.emplace_back(producer, i);
    }
    for (int i = 0; i < 5; ++i) {
        consumers.emplace_back(consumer, i);
    }

    for (auto& producer : producers) {
        producer.join();
    }
    for (auto& consumer : consumers) {
        consumer.join();
    }

    return 0;
}

この例では、3つのプロデューサースレッドがデータを生成し、5つのコンシューマースレッドがデータを消費します。std::shared_timed_mutexを使用して、データキューへのアクセスを同期しています。

データベースキャッシュの更新

マルチスレッド環境でのデータベースキャッシュの更新も、std::shared_timed_mutexを利用して効率的に実装できます。この例では、読み取りスレッドがキャッシュデータを読み取り、書き込みスレッドがデータベースからキャッシュを更新します。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
#include <unordered_map>
#include <string>

std::shared_timed_mutex cacheMutex;
std::unordered_map<int, std::string> cache;

std::string fetchDataFromDB(int id) {
    // シミュレートされたデータベースアクセス
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    return "Data" + std::to_string(id);
}

void reader(int id) {
    while (true) {
        {
            std::shared_lock<std::shared_timed_mutex> lock(cacheMutex);
            if (cache.find(id) != cache.end()) {
                std::cout << "Reader " << id << " reads: " << cache[id] << std::endl;
            } else {
                std::cout << "Reader " << id << " cache miss\n";
            }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

void writer() {
    for (int i = 0; i < 10; ++i) {
        std::string data = fetchDataFromDB(i);
        {
            std::unique_lock<std::shared_timed_mutex> lock(cacheMutex);
            cache[i] = data;
            std::cout << "Writer updates cache for id " << i << " with: " << data << std::endl;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    std::thread writerThread(writer);

    for (auto& reader : readers) {
        reader.join();
    }
    writerThread.join();

    return 0;
}

この例では、5つの読み取りスレッドがキャッシュからデータを読み取り、1つの書き込みスレッドがデータベースからキャッシュを更新します。std::shared_timed_mutexを使用して、キャッシュへの同時アクセスを管理しています。

ログシステムの実装

マルチスレッド環境でのログシステムの実装にも、std::shared_timed_mutexが有用です。ここでは、複数のスレッドがログを読み取り、書き込みスレッドがログを更新します。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
#include <string>
#include <deque>

std::shared_timed_mutex logMutex;
std::deque<std::string> logQueue;

void logReader(int id) {
    while (true) {
        {
            std::shared_lock<std::shared_timed_mutex> lock(logMutex);
            if (!logQueue.empty()) {
                std::cout << "Log Reader " << id << " reads log: " << logQueue.front() << std::endl;
            } else {
                std::cout << "Log Reader " << id << " no logs to read\n";
            }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

void logWriter() {
    for (int i = 0; i < 10; ++i) {
        std::string logEntry = "Log entry " + std::to_string(i);
        {
            std::unique_lock<std::shared_timed_mutex> lock(logMutex);
            logQueue.push_back(logEntry);
            std::cout << "Log Writer writes: " << logEntry << std::endl;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    std::vector<std::thread> logReaders;
    for (int i = 0; i < 3; ++i) {
        logReaders.emplace_back(logReader, i);
    }
    std::thread logWriterThread(logWriter);

    for (auto& logReader : logReaders) {
        logReader.join();
    }
    logWriterThread.join();

    return 0;
}

この例では、3つの読み取りスレッドがログを読み取り、1つの書き込みスレッドがログを更新します。std::shared_timed_mutexを使用して、ログキューへの同時アクセスを管理しています。

これらの応用例を通じて、std::shared_timed_mutexの柔軟性と有用性が理解できるでしょう。複雑なマルチスレッド環境でも効率的にデータの一貫性を保ちながら同期を取ることが可能です。

演習問題

ここでは、std::shared_timed_mutexの理解を深めるための演習問題をいくつか紹介します。これらの問題を解くことで、実際にstd::shared_timed_mutexを使用する際の具体的なシナリオを体験できます。

演習問題1: 基本的な読み取りと書き込み

以下のコードを完成させて、std::shared_timed_mutexを使用した読み取りおよび書き込み操作を実装してください。5つの読み取りスレッドと1つの書き込みスレッドが同時に動作するようにします。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void reader(int id) {
    // 共有ロックを取得してデータを読み取るコードを追加
}

void writer() {
    // 排他ロックを取得してデータを書き込むコードを追加
}

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    std::thread writerThread(writer);

    for (auto& reader : readers) {
        reader.join();
    }
    writerThread.join();

    return 0;
}

解答例

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void reader(int id) {
    for (int i = 0; i < 5; ++i) {
        std::shared_lock<std::shared_timed_mutex> lock(sharedMutex);
        std::cout << "Reader " << id << " reads sharedData: " << sharedData << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void writer() {
    for (int i = 0; i < 5; ++i) {
        std::unique_lock<std::shared_timed_mutex> lock(sharedMutex);
        sharedData++;
        std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    std::thread writerThread(writer);

    for (auto& reader : readers) {
        reader.join();
    }
    writerThread.join();

    return 0;
}

演習問題2: タイムアウト付きロックの実装

タイムアウト付きロックを使用して、一定時間内にロックを取得できなかった場合の処理を実装してください。読み取りスレッドと書き込みスレッドの両方でタイムアウトを使用します。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <chrono>
#include <vector>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void reader(int id) {
    // タイムアウト付き共有ロックを取得してデータを読み取るコードを追加
}

void writer() {
    // タイムアウト付き排他ロックを取得してデータを書き込むコードを追加
}

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    std::thread writerThread(writer);

    for (auto& reader : readers) {
        reader.join();
    }
    writerThread.join();

    return 0;
}

解答例

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <chrono>
#include <vector>

std::shared_timed_mutex sharedMutex;
int sharedData = 0;

void reader(int id) {
    for (int i = 0; i < 5; ++i) {
        if (sharedMutex.try_lock_shared_for(std::chrono::milliseconds(100))) {
            std::cout << "Reader " << id << " reads sharedData: " << sharedData << std::endl;
            sharedMutex.unlock_shared();
        } else {
            std::cout << "Reader " << id << " could not acquire lock\n";
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void writer() {
    for (int i = 0; i < 5; ++i) {
        if (sharedMutex.try_lock_for(std::chrono::milliseconds(100))) {
            sharedData++;
            std::cout << "Writer increments sharedData to: " << sharedData << std::endl;
            sharedMutex.unlock();
        } else {
            std::cout << "Writer could not acquire lock\n";
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

int main() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    std::thread writerThread(writer);

    for (auto& reader : readers) {
        reader.join();
    }
    writerThread.join();

    return 0;
}

演習問題3: データ構造の同期

複数のスレッドが同時にアクセスするデータ構造を同期するコードを実装してください。ここでは、std::unordered_mapを使用してデータを管理し、std::shared_timed_mutexを使って同期を取ります。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <unordered_map>
#include <string>
#include <vector>

std::shared_timed_mutex mapMutex;
std::unordered_map<int, std::string> dataMap;

void reader(int id) {
    // 共有ロックを取得してdataMapからデータを読み取るコードを追加
}

void writer(int id, const std::string& value) {
    // 排他ロックを取得してdataMapにデータを書き込むコードを追加
}

int main() {
    std::vector<std::thread> readers;
    std::vector<std::thread> writers;

    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    for (int i = 0; i < 3; ++i) {
        writers.emplace_back(writer, i, "Value" + std::to_string(i));
    }

    for (auto& reader : readers) {
        reader.join();
    }
    for (auto& writer : writers) {
        writer.join();
    }

    return 0;
}

解答例

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <unordered_map>
#include <string>
#include <vector>

std::shared_timed_mutex mapMutex;
std::unordered_map<int, std::string> dataMap;

void reader(int id) {
    for (int i = 0; i < 5; ++i) {
        std::shared_lock<std::shared_timed_mutex> lock(mapMutex);
        if (dataMap.find(id) != dataMap.end()) {
            std::cout << "Reader " << id << " reads: " << dataMap[id] << std::endl;
        } else {
            std::cout << "Reader " << id << " found no data\n";
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void writer(int id, const std::string& value) {
    for (int i = 0; i < 5; ++i) {
        std::unique_lock<std::shared_timed_mutex> lock(mapMutex);
        dataMap[id] = value;
        std::cout << "Writer " << id << " writes: " << value << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

int main() {
    std::vector<std::thread> readers;
    std::vector<std::thread> writers;

    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    for (int i = 0; i < 3; ++i) {
        writers.emplace_back(writer, i, "Value" + std::to_string(i));
    }

    for (auto& reader : readers) {
        reader.join();
    }
    for (auto& writer : writers) {
        writer.join();
    }

    return 0;
}

これらの演習問題を通じて、std::shared_timed_mutexの実用的な使い方を理解し、複雑なマルチスレッドプログラムでの同期を効果的に管理するスキルを身につけることができます。

まとめ

本記事では、C++のstd::shared_timed_mutexを使った複数読者単一著者ロックの仕組みとその利用方法について詳しく解説しました。std::shared_timed_mutexを使用することで、複数のスレッドが同時にデータを読み取る一方で、単一のスレッドがデータを書き込むことが可能になります。これは、読み取り頻度が高く、書き込み頻度が低いシナリオにおいて特に有効です。

具体的には、以下のポイントを学びました:

  • 基本概念: std::shared_timed_mutexの基本的な機能と用途を理解しました。
  • 基本的な使い方: 共有ロックと排他ロックの取得方法、およびタイムアウト付きロックの使用方法を具体的なコード例と共に学びました。
  • パフォーマンスの考慮: ロックの保持時間を最小限にする方法や、適切なデータ構造の選択について学びました。
  • 応用例: プロデューサー-コンシューマーモデルやデータベースキャッシュの更新など、実際のシナリオにおける応用例を紹介しました。
  • 演習問題: 理解を深めるための演習問題を通じて、実際にコードを書いて学ぶ機会を提供しました。

これらの知識を活用することで、効率的かつ安全なマルチスレッドプログラミングが実現できるようになります。std::shared_timed_mutexを上手に活用して、複雑なマルチスレッド環境でのデータ同期を効果的に管理しましょう。

コメント

コメントする

目次