C++の基礎 - マルチスレッド

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要

マルチプロセスとは違い、マルチスレッド環境ではメモリ空間を共有している。
そのため、共有リソースに対して適切な同期制御(排他制御)を行わないと共有リソースに対する更新処理を実行する場合に、データの不整合が発生する可能性がある。

これらの問題の解決するため、以下に示すような同期制御方法が存在する。

  • セマフォ
    • カウンティングセマフォ
    • バイナリセマフォ
  • Mutex
  • 条件変数 (Condition Value)


スレッド同期とは、2つ以上の同時実行プロセスまたはスレッドが、クリティカルセクション (プログラムの直列化されたセグメント) に対して同時に実行しないことを保証するメカニズムとして定義される。
プロセスによるクリティカルセクションへのアクセスは、同期技術を使用することで制御される。

あるスレッドがクリティカルセクションの実行を開始する時、他のスレッドは最初のスレッドが終了するまで待つ必要がある。

適切な同期技術が適用されていない場合、変数の値が予測不可能になったり、プロセスやスレッドのコンテキストスイッチのタイミングにより変化したりするレースコンディションが発生する。

一般的に、複数のスレッドからアクセスする変数等は排他制御が必須である。
また、複数のスレッドから参照されているクラスのメンバ変数にも排他制御は必要である。

例外としては、イミュータブルな変数およびstd::atomicのように既に排他制御が実施されているような変数である。


セマフォ

セマフォとは

異なるスレッド間で共有される変数等の共有資源に対する同時アクセス数 (あるリソースがいくつ使用可能かを示す値) を制限する機構のことである。

  • あるリソースを占有する時、セマフォの値を1減算する。
  • あるリソースの開放を行う時、セマフォの値を1加算する。


セマフォの値が正の数でなければそのリソースを占有することはできない。

ただし、セマフォにはリソースのロック所有権という概念が無いことに注意する。

カウンティングセマフォ

任意の個数の資源を扱うセマフォである。

カウンタ値に1加算した後、待機中スレッドを起こす。 (V操作、Up、Signal、Post)
カウンタ値が0より大きくなるまで待機した後、カウンタ値を1減算する。 (P操作、Down、Wait、Pend)

バイナリセマフォ

値が0と1に制限されているセマフォである。 (ロック / アンロック、使用可能 / 使用不可の意味)

バイナリセマフォにより、複数のスレッドから共有資源アクセスの相互排他 (Mutual Exclusion) 制御を実現することができる。

これは、Mutexでも実現可能である。
ただし、Mutexは複数のスレッド間の相互排除に特化しているため、Mutexを使うことを推奨する。


Mutex (Mutual exclusion)

Mutexとは

複数のスレッドから共有資源へのアクセス相互排他制御を実現する機構のことである。

あるタイミングにおいて、共有資源へアクセス可能なスレッドがただ1つしか存在しないことを保証する。

Mutexにより相互排他制御される対象は、共有資源としての変数やデータ構造である。
各スレッド上において実行されるプログラムのコード区間ではない。
したがって、あるスレッドの中であるリソースをロックして、待機状態時にそのスレッドが再び実行状態になる場合、
そのリソースに対してロックの解放を行わない限り、ロックされているリソースに対して他のスレッドがアクセスできない状態が続く。

ロック

Mutexでは、ロック / 非ロックの2つの状態をもつ。

他のスレッドがロックの所有権を解放するまで待機して、自スレッドがロックの所有権を獲得する。 (Lock、Acquire)
自スレッドが所有中のロックを解放して、同じミューテックスに対して待機中の他のスレッドへ通知する。 (Unlock、Release)

クリティカルセクション

あるスレッドがMutexのロックを所有した状態で実行するコード区間のことである

クリティカルセクションは、プログラム上で表現されているコード区間 (レキシカルスコープ) だけではなく、そのスレッドの実行パス上にある全てのコード区間 (ダイナミックスコープ) を指す。
あるMutexのロックの所有権を持った状態で呼び出した関数のコード区間は、関数呼び出し元で開始したクリティカルセクションに包含される。

適切な並列並行処理設計の下では、クリティカルセクションの範囲を可能な限り狭くするべきであり、ロックを所有したまま何らかの条件が満たされるまでスレッドを待機するという設計はしてはならない。
もし、そのような設計を行う場合は、条件変数を使用することを推奨する。

Mutexには、ロック所有権という概念が存在しており、ロック解放操作はロックを所有しているスレッドでのみ行うことができる。

例: Mutexを使用した排他制御

  • C++標準ライブラリ
    C++11以降は、std::mutex
  • Linux
    pthread_mutex系関数


C++標準ライブラリを使用した例
 #include <iostream>
 #include <mutex>
 #include <thread>
 
 void ThreadSample1();
 void ThreadSample2();
 void Add();
 
 std::mutex mtx;  // 排他制御向けMutex
 unsigned int count;
 
 int main(int argc, char *argv[])
 {
    count = 0;
 
    std::thread thread_a(ThreadSample1);
    std::thread thread_b(ThreadSample2);
 
    thread_a.join();
    thread_b.join();
 
    std::cout << "count : " << count << std::endl;
 
    return 0;
 }
 
 void ThreadSample1()
 {
    for (auto i = 0; i < 100000; i++) {
       Add();
    }
 }
 
 void ThreadSample2()
 {
    for (auto i = 0; i < 100000; i++) {
       Add();
    }
 }
 
 void Add()
 {
    // 変数countにアクセスする前にMutexを取得
    std::lock_guard<std::mutex> lock(mtx);
    count++;
 }


pthread_mutex系関数を使用した例
 #include <iostream>
 #include <cstdlib>
 #include <cstring>
 #include <unistd.h>
 #include <pthread.h>
 
 void* trythis(void *arg);
 
 pthread_t tid[2];
 pthread_mutex_t lock;
 int counter;
 
 int main(int argc, char *argv[])
 {
    int i = 0;
    int error;
 
    if (pthread_mutex_init(&lock, nullptr) != 0) {
        std::cout << "mutex init has failed" << std::endl;
 
        return 1;
    }
 
    while (i < 2) {
        error = pthread_create(&(tid[i]), nullptr, &trythis, nullptr);
        if (error != 0) {
            std::cout << "Thread can't be created : " << "[" << strerror(error)) << "]" << std::endl;
        }
 
        i++;
    }
 
    pthread_join(tid[0], nullptr);
    pthread_join(tid[1], nullptr);
    pthread_mutex_destroy(&lock);
 
    return 0;
 }
 
 void* trythis(void *arg)
 {
    pthread_mutex_lock(&lock);
 
    counter++;
    std::cout << "Job " << counter << " has started" <<  std::endl;
 
    for (auto i = 0; i < 100000; i++) ;
 
    std::cout << "Job " << counter << " has finished" <<  std::endl;
 
    pthread_mutex_unlock(&lock);
 
    return nullptr;
 }



条件変数

共有資源としてのデータ構造が、特定の状態に変化するまで待機する状況があるとする。
例えば、複数のスレッド間でデータを安全に送受信するFIFO待ち行列データ構造において、取り出し側は有効なデータが存在するまで待機、挿入側は空きができるまで待機する必要がある。

これを実現するものが条件変数である。
Mutexで保護されるデータ構造が、特定条件を満たすまで効率的に待機する機構の実装を助けることができる。

  • 関連付けられたMutexのロックを解放し、条件変数に通知があるまで自スレッドを待機して、再びロック獲得する。 (Wait)
  • 同じ条件変数に対して、待機中のスレッド群の内、ランダムな1スレッドに対して通知する。 (Notify、Signal)
  • 同じ条件変数に対して、待機中の全スレッドに対して通知する。 (NotifyAll、Broadcast)