[C++] 並行・並列処理に関連する標準ライブラリまとめ

C++は並行・並列処理を行うにあたってスレッドの管理や排他制御などの機能が標準ライブラリとして提供されています。
本記事ではそれらのライブラリをまとめて紹介します。並行・並列処理による未定義動作で1日無駄につぶす人が減るように願っています。

並行・並列処理とは

まず、並行処理と並列処理の違いについて軽く説明します。
並行処理は複数の処理が同時に実行可能であることで、実際に同時に実行されているかは関係ありません。例えばスレッドを2つ立ち上げるプログラムがあった場合は並行処理となります。というか最低でも並行処理といえます。
では、並列処理はどういうことかというと、実際に同時に動きます。最近のCPUであれば2コア以上実行コアが存在すると思いますが、並列処理はこのコアを同時に使い複数のスレッドやプロセスが同時に動作することを言います。
つまり、並列処理は確実に並行処理ですが、並行処理は並列処理といえません。
並列処理になるかどうかは環境(CPUやOS)などに依存するため、本記事では並行処理に関して考えます。

並行処理と未定義

並行処理でよくある未定義動作はデータ競合(data race)です。データ競合は次のような条件を満たすをおきます(単純のためいろいろ端折ってあります)。
  • 複数のスレッドが同時にアクセス
  • 最低でも一つが書き込み
  • 最低でも一つはアトミック操作でない
アトミック操作は不可分な操作で変数へのアクセスを分割しないで行います。CPUによっては専用の命令が存在します。
この条件はかなり満たしやすいです。ちょっとでも気を抜くと未定義の世界に足を踏み入れてしまうので、気を付けてください

競合状態(race condition)

データ競合と似た概念に競合状態というものがあります。データ競合と競合状態はレイヤーの違う話ですが、同じような文脈で語られることが多いので混同しやすいようです。
競合状態は操作が不可分であるものが実際には分割されてしまうときに起きます。
例えば、金銭の授受を行うアプリケーションを考えます。下のコードが例ですが、このプログラムはifで支払い能力があることを確認して授受を行います。この支払能力の確認と渡す側(from) から値を引くことは分割するとまずいことになります。
なので、いくらfromやto, amountがデータ競合を起こさないようにしてあったとしても、同時に関数が実行できてしまう場合は競合状態となり、1000回に1回しか起きないバグのようなことが起きてしまいます。
  void send_money(int& from, int& to, int amount) {
  	if(from >= amount) {
	  	from -= amount;
    	to += amount;
    }
  }
  

スレッドを立てる系

スレッドを立てる系の機能はstd::threadとstd::asyncがあります。
std::threadはまんまスレッドクラスです。std::asyncは非同期実行関数です。

<thread>: std::thread

std::threadはコンストラクタに関数呼び出し可能なオブジェクトや関数を渡すとスレッドを立ち上げ実行します。
このクラスはスレッドを立ち上げた際には破棄する前に必ずjoinとdetachを呼び出す必要があります。joinはスレッドの終了を待ちます。detachはスレッドを切り離します。

<future>: std::async

std::asyncは実行ポリシーを指定して関数を実行できます。実行ポリシーにはstd::launch::async(以後async)とstd::launch::deferred(以後deferred)が選べます。asyncはスレッドを立ち上げ実行します。deferredは遅延実行です。
この関数はstd::futureを返します。このstd::futureを通して返り値を受け取ることができます。futureのインスタンスを破棄したり値を取るときに処理が終了していなければブロックされます。

アトミック系 <atomic>

アトミック操作をするクラスとしてstd::atomic<T>があります。このクラスを使用することで操作をアトミックに行うことができます。

排他制御系

排他制御(MUTual EXclusion: mutex)はクリティカルセクションを作ることができる機能です。

<mutex>: std::mutex

std::mutexは一番単純なmutexクラスでlock()メンバ関数でリソースをロックし、unlock()メンバ関数でロックを解放します。

<shared_mutex>: std::shared_mutex (C++17)

std::shared_mutexはReaders-Writer-Lockができるクラスです。読み込みが多いときにstd::mutexを使うと読み込みしかしないのに待たされることが頻発します。このようなときに共有ロック(lock_shared())を行い複数のスレッドがロックを共有することができます。

<mutex>: std::lock_guard

std::lock_guardは破棄時に自動的にロックを解除してくれるクラスです。mutex版のunique_ptrみたいな機能です。動作はコンストラクタでlock()を呼び出し、デストラクタでunlock()を呼び出すシンプルなものです。

<mutex>: std::unique_lock

std::unique_lockはstd::lock_guardに似ていますがロックを取ったり破棄したりという関数が追加されています。

<shared_mutex>: std::shared_lock (C++14)

std::shared_lockはstd::unique_lockに似ていますが、lock_shared()とunlock_shared()に対するクラスである点で異なっています。つまりstd::shared_mutexで共有ロックを使うときはこちらのクラスを使用し、排他ロックを使うときはstd::unique_lockを使います。

条件変数 (condition variable) <condition_variable>

条件変数はイベントや条件を満たすまで待つためのクラスです。
wait系の関数でイベントや条件を待機し、notify系の関数で待機しているスレッドを復帰させます。

コメント

このブログの人気の投稿

初めの挨拶

C++11の機能 (型推論, 範囲for)

C++11の機能 (関数のdefault・delete宣言, overrideとfinal指定子, 移譲コンストラクタ, 継承コンストラクタ)