C++11 は、同時実行性を扱う最初の C++ 標準です。並行性の基本的な構成要素はスレッドです。したがって、ほとんどのルールは明示的にスレッドに関するものです。これは C++17 で劇的に変わりました。
C++17 を使用 標準テンプレート ライブラリ (STL) の並列アルゴリズムを取得しました。つまり、STL のほとんどのアルゴリズムは、順次、並列、またはベクトル化して実行できます。好奇心旺盛な読者のために:私はすでに並列 STL について 2 つの記事を書いています。標準テンプレート ライブラリの並列アルゴリズムの投稿では、既存のアルゴリズムを順次、並列、または並列に実行してベクトル化するために使用できる実行ポリシーについて説明しています。 C++17 では、並列実行またはベクトル化を目的とした新しいアルゴリズムも提供されました。詳細は次のとおりです:C++17:標準テンプレート ライブラリの新しい並列アルゴリズム。
C++ での並行処理の話は続きます。 C++20 では、Future、コルーチン、トランザクションなどの拡張が期待できます。俯瞰すると、C++11 と C++14 の同時実行機能は、C++17 と C++20 の高度な抽象化が基づいている実装の詳細にすぎません。これは、C++20 でのコンカレント フューチャーに関する一連の投稿です。
GCCもClangもMSVCもSTLの並列アルゴリズムを完全に実装していないため、ルールは主にスレッドに関するものであると述べました。利用できない機能 (並列 STL) や標準化されていない機能に対して書かれたベスト プラクティスは存在しません。
これは、ルールを読むときに覚えておくべき最初のルールです。これらのルールは、C++11 および C++14 で利用可能なマルチスレッドに関するものです。心に留めておくべき 2 番目のルールは、マルチスレッドは非常に難しいということです。これは、ルールがこの分野の専門家ではなく、初心者にガイダンスを提供することを意味します。メモリ モデルへのルールは、将来に従います。
それでは、最初のルールに飛び込みましょう。
CP.1:コードが一部として実行されると仮定するマルチスレッドプログラムの
初めてこのルールを読んだときは驚きました。特殊なケースに合わせて最適化する必要があるのはなぜですか?明確にするために、このルールは主に、アプリケーションではなくライブラリで使用されるコードに関するものです。また、経験から、ライブラリ コードは再利用されることが多いことがわかります。これは、一般的なケースに合わせて最適化する可能性があることを意味しますが、これで問題ありません。
ルールの要点を明確にするために、ここに小さな例を示します。
double cached_computation(double x) { static double cached_x = 0.0; // (1) static double cached_result = COMPUTATION_OF_ZERO; // (2) double result; if (cached_x == x) // (1) return cached_result; // (2) result = computation(x); cached_x = x; // (1) cached_result = result; // (2) return result; }
関数 cached_computation は、シングルスレッド環境で実行される場合はまったく問題ありません。静的変数 cached_x (1) および cached_result (2) は多くのスレッドで同時に使用でき、使用中に変更されるため、これはマルチスレッド環境には当てはまりません。 C++11 標準は、cached_x や cached_result などのブロック スコープを持つ静的変数にマルチスレッド セマンティクスを追加します。 ブロック スコープを持つ静的変数は、スレッド セーフな方法で C++11 で初期化されます。
これは問題ありませんが、私たちの場合には役に立ちません。多くのスレッドから同時に cached_computation を呼び出すと、データ競合が発生します。 C++ のマルチスレッドでは、データ競合の概念が非常に重要です。したがって、それについて書きましょう。
データ競争 少なくとも 2 つのスレッドが同時に共有変数にアクセスする状況です。少なくとも 1 つのスレッドが変数を変更しようとしています。
残りは非常に簡単です。プログラムにデータ競合がある場合、プログラムは未定義の動作をします。未定義の動作とは、すべてが発生する可能性があるため、プログラムについてこれ以上推論できないことを意味します。私はすべてを意味します。私のセミナーでは、「プログラムに未定義の動作がある場合、キャッチファイアのセマンティクスがある」とよく言います。コンピューターでさえ発火する可能性があります。
データ競合の定義を注意深く読むと、データ競合を発生させるには可変状態を共有する必要があることがわかります。これは、この観察結果を非常に明確にするための写真です。
では、データ競合をなくすにはどうすればよいでしょうか?静的変数 cached_x (1) および cached_result (2) を不変 (const) にすることは意味がありません。これは、両方の static を共有してはならないことを意味します。これを実現する方法をいくつか紹介します。
<オール>ここにバリエーション 1、2、3、および 4 があります。
std::mutex m_x; std::mutex m_result; double cached_computation(double x){ // (1) static double cached_x = 0.0; static double cached_result = COMPUTATION_OF_ZERO; double result; { std::scoped_lock(m_x, m_result); if (cached_x == x) return cached_result; } result = computation(x); { std::lock_guard<std::mutex> lck(m_x); cached_x = x; } { std::lock_guard<std::mutex> lck(m_result); cached_result = result; } return result; } std::mutex m; double cached_computation(double x){ // (2) static double cached_x = 0.0; static double cached_result = COMPUTATION_OF_ZERO; double result; { std::lock_guard<std::mutex> lck(m); if (cached_x == x) return cached_result; result = computation(x); cached_x = x; cached_result = result; } return result; } std::mutex cachedComputationMutex; // (3) { std::lock_guard<std::mutex> lck(cachedComputationMutex); auto cached = cached_computation(3.33); } double cached_computation(double x){ // (4) thread_local double cached_x = 0.0; thread_local double cached_result = COMPUTATION_OF_ZERO; double result; if (cached_x == x) return cached_result; result = computation(x); cached_x = x; cached_result = result; return result; }
まず、C++11 標準では、静的変数がスレッドセーフな方法で初期化されることが保証されています。したがって、すべてのプログラムで初期化を保護する必要はありません。
<オール>最終的には、パフォーマンスとユーザーの問題です。したがって、各バリエーションを試し、測定し、コードを使用および保守する必要がある人について考えてください。
次は?
この投稿は、ルールから C++ の同時実行性への長い旅の出発点にすぎません。次の投稿では、スレッドと共有状態について取り上げます。