【C++】5つのサンプルで学ぶunique_lockを使ったマルチスレッド同期の秘訣 – JPSM

【C++】5つのサンプルで学ぶunique_lockを使ったマルチスレッド同期の秘訣

C++のunique_lockを使ったマルチスレッドプログラミングのイメージC++

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

このサービスは複数のSSPによる協力の下、運営されています。

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

また、理解しにくい説明や難しい問題に躓いても、JPSMがプログラミングの解説に特化してオリジナルにチューニングした画面右下のAIアシスタントに質問していだければ、特殊な問題でも指示に従い解決できるように作ってあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

C++のマルチスレッドプログラミングは、ソフトウェアの性能を最大限に引き出すために不可欠です。

特に、複数のスレッドが共有リソースにアクセスする際の同期は重要な課題です。

この記事では、C++のunique_lockを使って、スレッド間の同期を効果的に行う方法を紹介します。

unique_lockをマスターすることで、マルチスレッド環境におけるより安全で効率的なプログラミングが可能になります。

●C++とunique_lockの基礎

C++は強力なシステムプログラミング言語で、マルチスレッドプログラミングをフルにサポートしています。

マルチスレッドプログラミングでは、複数のスレッドが並行して処理を行うことでアプリケーションのパフォーマンスを向上させることが可能です。

しかし、複数のスレッドが同時に同じデータにアクセスするとデータの不整合や競合が発生するリスクがあります。

これを防ぐためには、適切な同期メカニズムを用いる必要があります。

unique_lockは、C++のスタンダードライブラリに含まれるクラスで、mutex(相互排他)オブジェクトをより柔軟に制御できるように設計されています。

unique_lockを使うことで、mutexのロックとアンロックを自動的に管理できるため、プログラマはスレッドセーフなコードをより安全かつ簡単に記述できるようになります。

○unique_lockとは何か?

unique_lockは、C++のmutexをより柔軟に扱うためのツールです。

mutexは、複数のスレッドが同時に特定のリソースへのアクセスを試みることを防ぐために使用されます。

unique_lockを利用することで、mutexのロックとアンロックを自動で行うことができ、プログラムの安全性が向上します。

例えば、エラーが発生した場合でも、unique_lockがスコープを抜ける際に自動的にmutexを解放するため、デッドロックのリスクを減らすことができます。

また、unique_lockは、ロックの取得を遅延させる機能や、条件変数と組み合わせて使用することが可能です。

これにより、より複雑な同期処理を実現することができます。

○マルチスレッドプログラミングの基本

マルチスレッドプログラミングは、アプリケーションを複数のスレッドに分割して並行処理を行う手法です。

C++では<thread>ライブラリを用いてスレッドを生成し、管理することができます。

マルチスレッドプログラムを作成する際には、スレッドが同時に同じリソースにアクセスしないように注意を払う必要があります。

そのためには、mutexやunique_lockなどの同期メカニズムを利用して、データの競合や不整合を防ぐことが重要です。

また、スレッド間でのデータのやり取りや状態の同期も、マルチスレッドプログラミングの重要な要素です。

適切な同期処理を行うことで、効率的かつ安全なマルチスレッドプログラムを実現することが可能になります。

●unique_lockの使い方

C++におけるunique_lockの使い方を理解するには、まず基本的な概念とその運用方法を把握することが重要です。

unique_lockはmutexをより柔軟に制御し、スレッドセーフなコードを簡単に記述できるように設計されています。

主に、スコープベースでのロック管理を提供し、プログラムの安全性と効率を向上させます。

○基本的な使い方

unique_lockの基本的な使い方は、mutexと共に使用してスレッド間の同期を実現することです。

具体的には、unique_lockオブジェクトを作成し、そのコンストラクタにmutexオブジェクトを渡すことで、mutexのロックを取得します。

unique_lockオブジェクトのスコープを抜けると、デストラクタが自動的にmutexのロックを解放するため、スコープベースで安全にリソースを管理できます。

○サンプルコード1:基本的なロックの取得

例えば、下記のコードはunique_lockを使用して、mutexで保護されたデータに対するスレッドセーフなアクセスを実現しています。

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx; // グローバルmutex

void print_block(int n, char c) {
    std::unique_lock<std::mutex> lock(mtx);
    for (int i = 0; i < n; ++i) { std::cout << c; }
    std::cout << '\n';
}

int main() {
    std::thread th1(print_block, 50, '*');
    std::thread th2(print_block, 50, '$');

    th1.join();
    th2.join();

    return 0;
}

この例では、print_block 関数内でunique_lockを使用しています。

2つのスレッドがprint_block 関数を同時に呼び出しても、mutexが保証する排他制御により、データの不整合や競合を防いでいます。

○サンプルコード2:条件変数との組み合わせ

unique_lockは、条件変数と組み合わせて使用することがよくあります。

条件変数は、ある条件が満たされるまでスレッドの実行を停止させたり、条件が満たされたときにスレッドを再開させたりするのに使用されます。

unique_lockを条件変数と共に使用することで、より複雑な同期処理を実装することが可能になります。

下記のサンプルコードは、条件変数を使用して、特定の条件が満たされるのをスレッドが待機する方法を表しています。

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    while (!ready) cv.wait(lock);
    // スレッドはreadyがtrueになるまでここで待機する
    std::cout << "thread " << id << '\n';
}

void go() {
    std::unique_lock<std::mutex> lock(mtx);
    ready = true;
    cv.notify_all();
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(print_id, i);

    std::cout << "10 threads ready to race...\n";
    go(); // スレッドの実行を開始

    for (auto& th : threads) th.join();

    return 0;
}

このコードでは、print_id 関数内でcv.wait(lock)を使用し、ready変数がtrueになるまでスレッドの実行を停止しています。

go関数が呼ばれると、readyがtrueに設定され、cv.notify_all()により待機しているすべてのスレッドが再開されます。

●unique_lockの応用例

C++のunique_lockは、単にスレッド間の同期を行うだけではなく、より複雑なマルチスレッド処理の問題解決にも役立ちます。

ここでは、unique_lockの応用例として、リソース共有の管理、複数スレッドでのデータ処理、そしてデッドロックの回避について紹介します。

○サンプルコード3:リソース共有の管理

マルチスレッドプログラムでは、複数のスレッドが同じリソースへのアクセスを競うことがあります。

このような状況でリソースを効率的に管理するために、unique_lockを使用することができます。

例えば、複数のスレッドが共有データにアクセスするシナリオを考えてみましょう。

下記のコードでは、共有データへのアクセスをunique_lockで制御しています。

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx;
int shared_data = 0;

void increment_shared_data() {
    std::unique_lock<std::mutex> lock(mtx);
    ++shared_data;
    std::cout << "Data incremented to " << shared_data << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.push_back(std::thread(increment_shared_data));
    }

    for (auto& th : threads) {
        th.join();
    }

    return 0;
}

この例では、shared_dataが共有リソースであり、複数のスレッドがこのデータを同時に変更しようとします。

unique_lockを使用することで、一度に一つのスレッドのみがデータにアクセスし、データの整合性を保つことができます。

○サンプルコード4:複数スレッドでのデータ処理

マルチスレッドプログラムでは、複数のスレッドがデータを処理し、結果を共有することがよくあります。

下記のコードでは、複数のスレッドがそれぞれデータを処理し、処理結果を共有リソースに保存する例を表しています。

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <map>

std::mutex mtx;
std::map<int, int> results;

void compute(int id) {
    int result = id * id; // 何らかの計算
    std::unique_lock<std::mutex> lock(mtx);
    results[id] = result;
    std::cout << "Thread " << id << " computed " << result << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.push_back(std::thread(compute, i));
    }

    for (auto& th : threads) {
        th.join();
    }

    // 処理結果を表示
    for (const auto& pair : results) {
        std::cout << "Result[" << pair.first << "] = " << pair.second << std::endl;
    }

    return 0;
}

この例では、各スレッドが独立して計算を行い、その結果を共有のresultsマップに保存します。

unique_lockを使用することで、複数のスレッドが同じマップに安全にアクセスすることができます。

○サンプルコード5:デッドロックの回避

デッドロックは、複数のスレッドがお互いにリソースの解放を待っている状態であり、プログラムが停止する原因となります。

unique_lockは、スコープベースでのロック管理を提供することにより、デッドロックのリスクを軽減します。

下記の例では、デッドロックを避けるためにunique_lockを使用する方法を表しています。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx1, mtx2;

void thread_func_1() {
    std::unique_lock<std::mutex> lock1(mtx1);
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 時間差を作る
    std::unique_lock<std::mutex> lock2(mtx2);
    std::cout << "Thread 1 acquired both locks" << std::endl;
}

void thread_func_2() {
    std::unique_lock<std::mutex> lock2(mtx2);
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 時間差を作る
    std::unique_lock<std::mutex> lock1(mtx1);
    std::cout << "Thread 2 acquired both locks" << std::endl;
}

int main() {
    std::thread t1(thread_func_1);
    std::thread t2(thread_func_2);

    t1.join();
    t2.join();

    return 0;
}

この例では、二つのスレッドがそれぞれ異なる順序で二つのmutexをロックしようとします。

unique_lockを使用することで、一つのスレッドがすべての必要なリソースを取得するまで、他のスレッドは待機することになり、デッドロックを避けることができます。

●よくあるエラーと対処法

マルチスレッドプログラミング、特にC++のunique_lockを使用する際には、いくつかの一般的なエラーが発生する可能性があります。

ここでは、それらのエラーとそれらを解決する方法を具体的な例と共に解説します。

○エラー例と解決策1

unique_lockの使い方を誤ってデッドロックが発生するケースです。

デッドロックは、複数のスレッドが互いにロックの解放を待っている状態で、プログラムが停止してしまう問題です。

解決策として、デッドロックを避けるためには、ロックの取得順序を一貫させる、またはstd::lockを使って複数のmutexを同時にロックする方法があります。

下記のコードは、std::lockを使って複数のmutexをデッドロックを避けてロックする例です。

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mutex1, mutex2;

void threadFunction1() {
    std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
    std::lock(lock1, lock2); // std::lockを使って一度に複数のmutexをロック
    std::cout << "Thread 1 has acquired both locks." << std::endl;
}

void threadFunction2() {
    std::unique_lock<std::mutex> lock1(mutex2, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mutex1, std::defer_lock);
    std::lock(lock1, lock2); // std::lockを使って一度に複数のmutexをロック
    std::cout << "Thread 2 has acquired both locks." << std::endl;
}

int main() {
    std::thread t1(threadFunction1);
    std::thread t2(threadFunction2);

    t1.join();
    t2.join();

    return 0;
}

このコードでは、std::lockを使用してmutex1とmutex2をデッドロックを防ぎながらロックしています。

std::lockは、複数のmutexをデッドロックを発生させずに一度にロックするための関数です。

○エラー例と解決策2

unique_lockを使用する際に、適切にロックとアンロックを行わないことでリソースへのアクセスが不安定になるケースです。

これは特に、例外が発生した場合にロックが解放されないことで起こりえます。

解決策として、このような問題を避けるためには、unique_lockをスコープベースで使用することが重要です。

例外が発生しても、unique_lockのデストラクタが自動的にmutexを解放し、リソースの安全な管理を保証します。

下記のコードは、例外処理を含むunique_lockの使用例です。

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;

void process_data() {
    std::unique_lock<std::mutex> lock(mtx);
    // データ処理
    // ...

    throw std::runtime_error("Error occurred");
    // 例外が発生しても、unique_lockのデストラクタによりロックは解放される
}

int main() {
    try {
        std::thread t(process_data);
        t.join();
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }
    return 0;
}

この例では、process_data 関数内で例外が発生していますが、unique_lockのデストラクタによって自動的にロックが解放されるため、デッドロックやリソースリークを防いでいます。

このようにunique_lockを適切に使用することで、マルチスレッド環境での安全なリソース管理を確保できます。

●unique_lockの豆知識

unique_lockはC++のマルチスレッドプログラミングにおいて非常に役立つツールですが、その特性や他のロックメカニズムとの違いを理解することは、より効果的なプログラムを作成する上で重要です。

ここでは、unique_lockに関する興味深い豆知識と、他のロックとの比較について掘り下げていきます。

○豆知識1:パフォーマンスに関する考察

unique_lockは柔軟性が高い反面、そのオーバーヘッドは他のロックメカニズムよりも大きくなる可能性があります。

特に、軽量な操作に対してunique_lockを使う場合、そのオーバーヘッドが顕著になることがあります。

unique_lockのオーバーヘッドは、ロックの取得と解放のプロセスに起因するもので、頻繁にロックを取得・解放する場合には、パフォーマンスへの影響を検討する必要があります。

例えば、高頻度で短期間のロックを必要とする場合、std::lock_guardの方が適している場合があります。

std::lock_guardはunique_lockよりもシンプルな構造を持ち、オーバーヘッドが少ないため、パフォーマンスが重要な場面では選択肢として考慮されます。

○豆知識2:unique_lockと他のロックの比較

unique_lockと他のロックメカニズム、例えばstd::lock_guardやstd::scoped_lockとの比較を行うことで、各々のユースケースを理解することができます。

std::lock_guardは最もシンプルなmutexラッパーで、スコープに基づいたロック管理を提供しますが、unique_lockのような柔軟性はありません。

std::scoped_lockはC++17で導入され、複数のmutexに対するデッドロックを防ぐ機能を提供しますが、unique_lockのようにロックの取得を遅延させることはできません。

unique_lockはこれらのロックメカニズムと比較して、下記のような特徴があります。

  • ロックの遅延取得や条件付きロック、手動でのロック解放など、より柔軟な操作が可能です
  • 条件変数との連携が容易で、複雑な同期処理に適しています
  • 他のロックメカニズムと比べてオーバーヘッドが大きくなる可能性がありますが、それを補うだけの柔軟性と機能を提供します

これらの違いを理解することで、プログラマは各シナリオに最適なロックメカニズムを選択することができます。

軽量な操作にはstd::lock_guardやstd::scoped_lockを、より複雑で柔軟性を要求する場合にはunique_lockを使用するなど、状況に応じた選択が重要です。

まとめ

この記事では、C++のunique_lockを使ったマルチスレッドプログラミングの基本から応用までを幅広く解説しました。

初心者から上級者まで、unique_lockの基本的な使い方、応用例、よくあるエラーとその対処法、そして豆知識までを網羅的に理解することができます。

この知識を活用して、安全かつ効率的なマルチスレッドプログラムの開発に役立ててください。

C++でのマルチスレッド処理の深い理解を目指し、より高度なプログラミング技術の習得につなげていきましょう。