MENU

【C++】Mutexによる排他制御について~概要とサンプルコード~

C++の勉強におすすめの書籍はこちらです。

[改訂第5版]C ポケットリファレンス (Pocket reference)

新品価格
¥3,520から
(2024/6/20 20:39時点)

Effective Modern C ―C 11/14プログラムを進化させる42項目

新品価格
¥4,180から
(2024/6/20 20:40時点)

Effective C 第3版 (ADDISON-WESLEY PROFESSIONAL COMPUTI)

新品価格
¥4,180から
(2024/6/20 20:40時点)

目次

はじめに

排他制御とは、複数のプロセスやスレッドが、メモリやファイルといった共有リソースに同時にアクセスする際に、データの整合性を保つための仕組みや手法のことを指します。排他制御の目的は、共有リソースに対する競合状態を防ぎ、予期しない動作やデータの破損を避けることです。

排他制御の代表的な手法には以下のようなものがありますが、この記事ではMutexについて解説し、実際の使い方を紹介したいと思います。

  • Mutex
  • セマフォ
  • リード・ライトロック
  • クリティカルセクション

排他制御は、特にマルチスレッドプログラミングや並列処理において重要な概念です。正しく実装されないと、デッドロック(プロセスやスレッドが相互に待ち状態に陥る状態)やスタベーション(あるスレッドが永久にリソースを得られない)などの問題が発生する可能性があります。そのため、排他制御のメカニズムを適切に理解し、正しく活用することが重要です。

C++でマルチスレッドプログラミングをする方法は以下の記事で紹介していますので、マルチスレッドがどういうものかわからない方はご覧ください。

Mutexとは

Mutexは、最も一般的な排他制御手法です。「Mutual Exclusion」の略で、一度に一つのスレッドだけがリソースにアクセスできるようにするロック機構です。あるスレッドがミューテックスを取得すると、他のスレッドはそのミューテックスが解放されるまで待つ必要があります。

ミューテックスの基本操作

ロック(lock)

  • 他のスレッドがMutexのロックを取得していない場合、ロックを取得し、クリティカルセクションに入ります。クリティカルセッションとは、プログラム内で一度に一つのスレッドだけが実行できるコードの部分を指します。そのため、クリティカルセッションの中でのみ共有リソースの操作を行うことで、排他制御を実現することができます。
  • Mutexが既にロックされている場合、そのスレッドはMutexが解放されるまで待機します。

アンロック(unlock)

  • スレッドがクリティカルセクションを出る際にMutexを解放します。これにより、他の待機中のスレッドがMutexを取得できるようになります。

サンプルコード

C++の標準ライブラリ<mutex>を使って、簡単な排他制御のプログラムを書いてみましょう。

2つのスレッドによるカウンタのインクリメント

以下のコードでは、2つのスレッドが共有のカウンタをインクリメントします。、2つのスレッドが同時にカウンタをインクリメントするのを防ぐためにMutexを使用します。

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

int counter = 0;          // 共有カウンタ
std::mutex counterMutex;  // ミューテックス

void incrementCounter(int id) {
    for (int i = 0; i < 100; ++i) {
        // ミューテックスのロック
        std::lock_guard<std::mutex> lock(counterMutex);
        int temp = counter; // カウンタの現在値を読み込む
        std::this_thread::sleep_for(std::chrono::microseconds(1)); // 遅延を追加
        counter = temp + 1; // カウンタをインクリメント
        std::cout << "Thread " << id << " incremented counter to " << counter << std::endl;
    } // lock_guardがスコープを抜けるとミューテックスは自動的にアンロックされる
}

int main() {
    std::thread thread1(incrementCounter, 1);
    std::thread thread2(incrementCounter, 2);

    thread1.join();
    thread2.join();

    std::cout << "Final counter value: " << counter << std::endl;

    return 0;
}

ヘッダーのインクルード

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

<iostream>は標準入出力ストリームを扱うため、<thread>はマルチスレッドを扱うため、<mutex>はMutexを扱うために使用します。

共有リソースとミューテックスの定義

int counter = 0;          // 共有カウンタ
std::mutex counterMutex;  // ミューテックス

counterは共有されるカウンタ変数で、counterMutexはこのカウンタを保護するためのMutexです。

スレッド関数の定義

void incrementCounter(int id) {
    for (int i = 0; i < 100; ++i) {
        // ミューテックスのロック
        std::lock_guard<std::mutex> lock(counterMutex);
        int temp = counter; // カウンタの現在値を読み込む
        std::this_thread::sleep_for(std::chrono::microseconds(1)); // 遅延を追加
        counter = temp + 1; // カウンタをインクリメント
        std::cout << "Thread " << id << " incremented counter to " << counter << std::endl;
    } // lock_guardがスコープを抜けるとミューテックスは自動的にアンロックされる
}

incrementCounter関数は、指定された回数カウンタをインクリメントします。std::lock_guardを使って、Mutexを自動的にロックおよびアンロックします。lock_guardがスコープを抜けると自動的にアンロックされるため、安全に排他制御が行えます。

int temp = counter; // カウンタの現在値を読み込む
std::this_thread::sleep_for(std::chrono::microseconds(1)); // 遅延を追加
counter = temp + 1; // カウンタをインクリメント

この部分では、スレッド間での競合を発生しやすくするために、あえて無駄な処理をしたり遅延を入れたりしています。

スレッドの作成と開始

std::thread thread1(incrementCounter, 1);
std::thread thread2(incrementCounter, 2);

2つのスレッドを作成し、それぞれにincrementCounter関数を渡して開始しています。第2引数はスレッドIDです。

スレッドの完了を待機

thread1.join();
thread2.join();

join()メソッドを呼び出すことで、スレッドの終了を待機します。

実行結果

このプログラムを実行すると以下のメッセージが標準出力に吐かれます。Mutexでカウンタを排他しているため、最終的にカウンタの値が200になっていますね。

Thread 1 incremented counter to 1
Thread 2 incremented counter to 2
Thread 2 incremented counter to 3

// 途中は省略

Thread 1 incremented counter to 198
Thread 1 incremented counter to 199
Thread 1 incremented counter to 200
Final counter value: 200

ちなみにMutexによる排他を行わずに実行すると以下のようになります。排他制御をしていないために競合が発生し、正確にカウンタをインクリメントできていないですね。

Thread 1 incremented counter to 1
Thread 2 incremented counter to 1
Thread 2 incremented counter to 2

// 途中は省略

Thread 2 incremented counter to 100
Thread 2 incremented counter to 101
Thread 2 incremented counter to 102
Final counter value: 102

さいごに

マルチスレッドプログラミングでは、適切な排他制御が重要になります。この記事では、排他制御の基本的な概念と代表的な排他手法であるMutexについて紹介しました。

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

この記事を書いた人

ソフトウェアエンジニアとして働いています。

---------------------資格---------------------
応用情報技術者
ネットワークスペシャリスト

目次