C++ で std::mutex 同期プリミティブを使用する

胡金庫 2023年10月12日
C++ で std::mutex 同期プリミティブを使用する

この記事では、C++ で std::mutex 同期プリミティブを使用する方法を示します。

std::mutex を使用して、C++ のスレッド間の共有データへのアクセスを保護する

一般に、同期プリミティブは、並行性を利用するプログラムで共有データへのアクセスを安全に制御するためのプログラマー向けのツールです。

複数のスレッドからの共有メモリ位置の順序付けられていない変更は、誤った結果と予測できないプログラム動作をもたらすため、プログラムが決定論的な方法で実行されることを保証するのはプログラマーの責任です。並行プログラミングの同期およびその他のトピックは非常に複雑であり、多くの場合、最新のコンピューティングシステムのソフトウェアおよびハードウェアの特性の複数のレイヤーに関する広範な知識が必要です。

したがって、この記事の同期トピックのごく一部をカバーしながら、これらの概念に関する以前の知識があることを前提としています。つまり、ミューテックスとも呼ばれる相互排除の概念を紹介します(多くの場合、std::mutex などのプログラミング言語では、オブジェクト名に同じ名前が付けられます)。

ミューテックスは、プログラムのクリティカルセクションを囲み、そのセクションへのアクセスを確実に保護できるロックメカニズムの一種です。共有リソースが保護されているとは、1つのスレッドが共有オブジェクトに対して書き込み操作を実行している場合、前のスレッドが操作を終了するまで他のスレッドは操作されないことを意味します。

このような動作は、リソースの競合、リソースの不足、またはその他のパフォーマンス関連の問題が発生する可能性があるため、一部の問題には最適ではない可能性があることに注意してください。したがって、他のいくつかのメカニズムはこれらの問題に対処し、この記事の範囲を超えて異なる特性を提供します。

次の例では、C++ STL によって提供される std::mutex クラスの基本的な使用法を示します。C++ 11 バージョン以降、スレッドの標準サポートが追加されていることに注意してください。

最初に、共有リソースへのアクセスを制御するために使用できる std::mutex オブジェクトを作成する必要があります。std::mutex には、lockunlock の 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つのスレッドがそのワークユニットを完了すると、次の保留中のワークユニットで再利用できます。ただし、ドライバーコードは、プログラム内のマルチスレッドワークフローの単純なシミュレーションとしてのみ設計されています。

著者: 胡金庫
胡金庫 avatar 胡金庫 avatar

DelftStack.comの創設者です。Jinku はロボティクスと自動車産業で8年以上働いています。自動テスト、リモートサーバーからのデータ収集、耐久テストからのレポート作成が必要となったとき、彼はコーディングスキルを磨きました。彼は電気/電子工学のバックグラウンドを持っていますが、組み込みエレクトロニクス、組み込みプログラミング、フロントエンド/バックエンドプログラミングへの関心を広げています。

LinkedIn Facebook