C++ で std::mutex 同期プリミティブを使用する
この記事では、C++ で std::mutex
同期プリミティブを使用する方法を示します。
std::mutex
を使用して、C++ のスレッド間の共有データへのアクセスを保護する
一般に、同期プリミティブは、並行性を利用するプログラムで共有データへのアクセスを安全に制御するためのプログラマー向けのツールです。
複数のスレッドからの共有メモリ位置の順序付けられていない変更は、誤った結果と予測できないプログラム動作をもたらすため、プログラムが決定論的な方法で実行されることを保証するのはプログラマーの責任です。並行プログラミングの同期およびその他のトピックは非常に複雑であり、多くの場合、最新のコンピューティングシステムのソフトウェアおよびハードウェアの特性の複数のレイヤーに関する広範な知識が必要です。
したがって、この記事の同期トピックのごく一部をカバーしながら、これらの概念に関する以前の知識があることを前提としています。つまり、ミューテックス
とも呼ばれる相互排除の概念を紹介します(多くの場合、std::mutex
などのプログラミング言語では、オブジェクト名に同じ名前が付けられます)。
ミューテックスは、プログラムのクリティカルセクションを囲み、そのセクションへのアクセスを確実に保護できるロックメカニズムの一種です。共有リソースが保護されているとは、1つのスレッドが共有オブジェクトに対して書き込み操作を実行している場合、前のスレッドが操作を終了するまで他のスレッドは操作されないことを意味します。
このような動作は、リソースの競合、リソースの不足、またはその他のパフォーマンス関連の問題が発生する可能性があるため、一部の問題には最適ではない可能性があることに注意してください。したがって、他のいくつかのメカニズムはこれらの問題に対処し、この記事の範囲を超えて異なる特性を提供します。
次の例では、C++ STL によって提供される std::mutex
クラスの基本的な使用法を示します。C++ 11 バージョン以降、スレッドの標準サポートが追加されていることに注意してください。
最初に、共有リソースへのアクセスを制御するために使用できる std::mutex
オブジェクトを作成する必要があります。std::mutex
には、lock
と unlock
の 2つのコアメンバー関数があります。lock
操作は通常、共有リソースが変更される前に呼び出され、ロック解除
は変更後に呼び出されます。
これらの呼び出しの間に挿入されるコードは、クリティカルセクションと呼ばれます。以前のコードレイアウトの順序は正しいですが、C++ は別の便利なテンプレートクラス std::lock_guard
を提供します。これは、スコープが離れると、指定された mutex
のロックを自動的に解除できます。lock
および unlock
メンバー関数を直接使用する代わりに lock_guard
を使用する主な理由は、例外が発生した場合でも、すべてのコードパスで mutex
がロック解除されることを保証するためです。したがって、コード例でも後者の方法を使用して、std::mutex
の使用法を示します。
main
プログラムは、ランダムな整数の 2つの vector
コンテナを作成し、両方のコンテンツをリストにプッシュするように構築されています。トリッキーな部分は、リストに要素を追加するために複数のスレッドを利用したいということです。
実際には、2つのスレッドで generateNumbers
関数を呼び出しますが、これらは異なるオブジェクトで動作するため、同期は不要です。整数を生成したら、addToList
関数を呼び出してリストに入力できます。
この関数は lock_guard
構造で始まり、リストで実行する必要のある操作が含まれていることに注意してください。この場合、指定されたリストオブジェクトで push_back
関数のみを呼び出します。
#include <chrono>
#include <iostream>
#include <list>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using std::cout;
using std::endl;
using std::list;
using std::string;
using std::vector;
std::mutex list1_mutex;
const int MAX = 1000;
const int NUMS_TO_GENERATE = 1000000;
void addToList(const int &num, list<int> &l) {
std::lock_guard<std::mutex> guard(list1_mutex);
l.push_back(num);
}
void generateNumbers(vector<int> &v) {
for (int n = 0; n < NUMS_TO_GENERATE; ++n) {
v.push_back(std::rand() % MAX);
}
}
int main() {
list<int> list1;
vector<int> vec1;
vector<int> vec2;
std::thread t1(generateNumbers, std::ref(vec1));
std::thread t2(generateNumbers, std::ref(vec2));
t1.join();
t2.join();
cout << vec1.size() << ", " << vec2.size() << endl;
for (int i = 0; i < NUMS_TO_GENERATE; ++i) {
std::thread t3(addToList, vec1[i], std::ref(list1));
std::thread t4(addToList, vec2[i], std::ref(list1));
t3.join();
t4.join();
}
cout << "list size = " << list1.size() << endl;
return EXIT_SUCCESS;
}
出力:
1000000, 1000000
list size = 2000000
前のコードスニペットでは、for
ループの反復ごとに 2つの別々のスレッドを作成し、同じサイクルでそれらをメインスレッドに結合することを選択しました。このシナリオは、スレッドの作成と破棄に貴重な実行時間がかかるため非効率的ですが、ミューテックス
の使用法を示すためにのみ提供しています。
通常、プログラム内の任意の時間に複数のスレッドを管理する必要がある場合は、スレッドプールの概念が利用されます。この概念の最も単純な形式は、作業ルーチンの開始時に固定数のスレッドを作成し、キューのような方法でそれらに作業単位の割り当てを開始します。1つのスレッドがそのワークユニットを完了すると、次の保留中のワークユニットで再利用できます。ただし、ドライバーコードは、プログラム内のマルチスレッドワークフローの単純なシミュレーションとしてのみ設計されています。