Share via


並列コンテナーと並列オブジェクト

並列パターン ライブラリ (PPL) には、要素へのスレッド セーフなアクセスを提供するコンテナーとオブジェクトがいくつか含まれています。

コンカレント コンテナーは、最も重要な操作へのコンカレンシー セーフなアクセスを提供します。 ここでは、コンカレンシー セーフは、ポインターまたは反復子が常に有効であることを意味します。 要素の初期化または特定の走査順序が保証されるわけではありません。 これらのコンテナーの機能は、C++ 標準ライブラリで提供される機能と似た機能です。 たとえば、concurrency::concurrent_vector クラスは std::vector クラスと似ています。ただし、concurrent_vector クラスでは要素を並列で追加できます。 同じコンテナーへの読み取りアクセスと書き込みアクセスの両方を必要とする並列コードがある場合は、コンカレント コンテナーを使用します。

コンカレント オブジェクトは、コンポーネント間で同時に共有されます。 コンカレント オブジェクトの状態を並列で計算するプロセスでは、同じ状態を連続して計算する別のプロセスと同じ結果が生成されます。 concurrency::combinable クラスは、コンカレント オブジェクト型の 1 つの例です。 combinable クラスを使用すると、計算を並列で実行し、それらの計算を最終的な結果に結合できます。 ミューテックスなどの同期メカニズムを使用する場合にはコンカレント オブジェクトを使用して、共有変数またはリソースへのアクセスを同期します。

セクション

このトピックでは、次の並列コンテナーと並列オブジェクトについて詳しく説明します。

コンカレント コンテナー:

コンカレント オブジェクト:

concurrent_vector クラス

concurrency::concurrent_vector クラスは、std::vector クラスと同様に、要素にランダムにアクセスできるシーケンス コンテナー クラスです。 concurrent_vector クラスを使用すると、コンカレンシーセーフな追加操作と要素アクセス操作が可能になります。 追加操作は、既存のポインターまたは反復子を無効にしません。 反復子アクセスと走査操作もコンカレンシーセーフです。 ここでは、コンカレンシー セーフは、ポインターまたは反復子が常に有効であることを意味します。 要素の初期化または特定の走査順序が保証されるわけではありません。

concurrent_vector と vector の違い

concurrent_vector クラスは vector クラスによく似ています。 concurrent_vector オブジェクトに対する追加、要素アクセス、反復子のアクセス操作の複雑さは、vector オブジェクトの場合と同じです。 次に concurrent_vectorvector の違いを示します。

  • concurrent_vector オブジェクトの追加、要素アクセス、反復子アクセス、反復子走査操作はコンカレンシーセーフです。

  • concurrent_vector オブジェクトの末尾にのみ要素を追加できます。 concurrent_vector クラスは、insert メソッドを提供しません。

  • concurrent_vector オブジェクトは、これに追加する場合、移動セマンティクスを使用しません。

  • concurrent_vector クラスは、erase メソッドまたは pop_back メソッドを提供しません。 vector と同様に、クリア メソッドを使用して、concurrent_vector オブジェクトからすべての要素を削除します。

  • concurrent_vector クラスは、その要素をメモリに連続して格納しません。 したがって、配列を使用できるすべての方法で concurrent_vector クラスを使用することはできません。 たとえば、concurrent_vector 型の v という名前の変数では、式 &v[0]+2 の動作は不定になります。

  • concurrent_vector クラスは、grow_by メソッドと grow_to_at_least メソッドを定義します。 これらのメソッドは、コンカレンシーセーフである場合を除き、resize メソッドに似ています。

  • concurrent_vector オブジェクトに追加したりサイズを変更したりすると、このオブジェクトは要素を再配置しません。 これにより、既存のポインターと反復子をコンカレント操作中も有効なままです。

  • このランタイムは、bool 型に対して concurrent_vector の特殊化バージョンを定義しません。

コンカレンシーセーフ操作

concurrent_vector オブジェクトのサイズを追加または増やす、または concurrent_vector オブジェクト内の要素にアクセスするメソッドは、すべてコンカレンシーセーフです。 ここでは、コンカレンシー セーフは、ポインターまたは反復子が常に有効であることを意味します。 要素の初期化または特定の走査順序が保証されるわけではありません。 この規則の例外は、resize メソッドです。

次の表は、コンカレンシーセーフな一般的な concurrent_vector メソッドと演算子を示しています。

ランタイムが C++ 標準ライブラリとの互換性のために提供する操作 (reserve など) は、コンカレンシーセーフではありません。 次の表は、コンカレンシーセーフではない一般的なメソッドと演算子を示しています。

既存の要素の値を変更する操作は、コンカレンシーセーフではありません。 reader_writer_lock オブジェクトなどの同期オブジェクトを使用して、コンカレント読み取り操作と書き込み操作を同じデータ要素に同期します。 同期オブジェクトの詳細については、「同期データ構造」をご覧ください。

vector を使用して concurrent_vector を使用する既存のコードを変換すると、コンカレント操作によってアプリケーションの動作が変更される可能性があります。 たとえば、concurrent_vector オブジェクトに対して 2 つのタスクを同時に実行する次のプログラムを考えてみましょう。 最初のタスクは、concurrent_vector オブジェクトに追加の要素を追加します。 2 番目のタスクは、同じオブジェクト内のすべての要素の合計を計算します。

// parallel-vector-sum.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_vector.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
   // Create a concurrent_vector object that contains a few
   // initial elements.
   concurrent_vector<int> v;
   v.push_back(2);
   v.push_back(3);
   v.push_back(4);
   
   // Perform two tasks in parallel.
   // The first task appends additional elements to the concurrent_vector object.
   // The second task computes the sum of all elements in the same object.

   parallel_invoke(
      [&v] { 
         for(int i = 0; i < 10000; ++i)
         {
            v.push_back(i);
         }
      },
      [&v] {
         combinable<int> sums;
         for(auto i = begin(v); i != end(v); ++i) 
         {
            sums.local() += *i;
         }     
         wcout << L"sum = " << sums.combine(plus<int>()) << endl;
      }
   );
}

end メソッドはコンカレンシーセーフですが、push_back メソッドのコンカレント呼び出しを行うと、end によって返される値が変更されます。 反復子が走査する要素の数は不確定です。 そのため、このプログラムは、実行するたびに結果が変わる可能性があります。 要素型が簡単でない場合、push_backend の呼び出しの間に競争状態が発生する可能性があります。 end メソッドは、割り当てられているが、完全には初期化されていない要素を返す場合があります。

例外の安全性

増加操作または割り当て操作で例外がスローされた場合、concurrent_vector オブジェクトの状態は無効になります。 無効な状態にある concurrent_vector オブジェクトの動作は、特に明記されていない限り不定になります。 ただし、デストラクターは、オブジェクトが無効な状態でも、オブジェクトが割り当てるメモリを常に解放します。

ベクター要素のデータ型 T は、次の要件を満たしている必要があります。 満たしていない場合、concurrent_vector クラスの動作は不定になります。

  • デストラクターはスローしない必要があります。

  • 既定のコンストラクターまたはコピーコンストラクターがスローした場合、デストラクターは virtual キーワードを使用して宣言する必要はありません。また、ゼロ初期化メモリで正しく動作する必要があります。

[トップ]

concurrent_queue クラス

concurrency::concurrent_queue クラスは、std::queue クラスと同様にフロント要素とバック要素にアクセスできます。 concurrent_queue クラスを使用すると、コンカレンシーセーフなエンキュー操作とデキュー操作が可能になります。 ここでは、コンカレンシー セーフは、ポインターまたは反復子が常に有効であることを意味します。 要素の初期化または特定の走査順序が保証されるわけではありません。 concurrent_queue クラスは、コンカレンシーセーフではない反復子のサポートも提供します。

concurrent_queue と queue の違い

concurrent_queue クラスは queue クラスによく似ています。 次に concurrent_queuequeue の違いを示します。

  • concurrent_queue オブジェクトに対するエンキュー操作とデキュー操作は、コンカレンシーセーフです。

  • concurrent_queue クラスは、コンカレンシーセーフではない反復子のサポートも提供します。

  • concurrent_queue クラスは、front メソッドまたは pop メソッドを提供しません。 concurrent_queue クラスは、try_pop メソッドを定義することで、これらのメソッドを置き換えます。

  • concurrent_queue クラスは、back メソッドを提供しません。 したがって、キューの末尾を参照することはできません。

  • concurrent_queue クラスは、sizeメソッドの代わりに unsafe_size メソッドを提供します。 unsafe_size メソッドはコンカレンシーセーフではありません。

コンカレンシーセーフ操作

concurrent_queue オブジェクトに対してエンキューまたはデキューを行うすべてのメソッドは、コンカレンシーセーフです。 ここでは、コンカレンシー セーフは、ポインターまたは反復子が常に有効であることを意味します。 要素の初期化または特定の走査順序が保証されるわけではありません。

次の表は、コンカレンシーセーフな一般的な concurrent_queue メソッドと演算子を示しています。

empty メソッドはコンカレンシーセーフですが、コンカレント操作によって empty メソッドが戻る前にキューが拡大または縮小する場合があります。

次の表は、コンカレンシーセーフではない一般的なメソッドと演算子を示しています。

反復子のサポート

concurrent_queue は、コンカレンシーセーフではない反復子を提供します。 これらの反復子は、デバッグのためにのみ使用することをお勧めします。

concurrent_queue 反復子は、前方方向にのみ要素を走査します。 次の表は、各反復子がサポートする演算子を示しています。

Operator 説明
operator++ キューの次の項目に進みます。 この演算子は、前置インクリメントと後置インクリメントの両方のセマンティクスを提供するためにオーバーロードされています。
operator* 現在の項目への参照を取得します。
operator-> 現在の項目へのポインターを取得します。

[トップ]

concurrent_unordered_map クラス

concurrency::concurrent_unordered_map クラスは、std::unordered_map クラスと同様の連想コンテナー クラスであり、std::pair<const Key 型 Ty> の要素の可変長シーケンスを制御します。 順序なしのマップは、キーと値のペアを追加したり、キーで値を参照したりできるディクショナリと考えることができます。 このクラスは、共有コンテナーに同時にアクセスしたり、それに挿入したり、更新したりする必要がある複数のスレッドまたはタスクがある場合に便利です。

次の例は、concurrent_unordered_map を使用するための基本的な構造を示しています。 この例では、['a', 'i'] の範囲内に文字キーを挿入します。 操作の順序は不明であるため、各キーの最終的な値も不定になります。 ただし、挿入を並列で実行するのは安全です。

// unordered-map-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_map.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the map in parallel.

    concurrent_unordered_map<char, int> map; 

    parallel_for(0, 1000, [&map](int i) {
        char key = 'a' + (i%9); // Geneate a key in the range [a,i].
        int value = i;          // Set the value to i.
        map.insert(make_pair(key, value));
    });

    // Print the elements in the map.
    for_each(begin(map), end(map), [](const pair<char, int>& pr) {
        wcout << L"[" << pr.first << L", " << pr.second << L"] ";
    });
}
/* Sample output:
    [e, 751] [i, 755] [a, 756] [c, 758] [g, 753] [f, 752] [b, 757] [d, 750] [h, 754]
*/

concurrent_unordered_map を使用してマップ操作と縮小操作を並列で実行する例については、「方法: マップ操作と縮小操作を並列実行する」をご覧ください。

concurrent_unordered_map と unordered_map の違い

concurrent_unordered_map クラスは unordered_map クラスによく似ています。 次に concurrent_unordered_mapunordered_map の違いを示します。

  • erasebucketbucket_count、および bucket_size メソッドは、それぞれ、unsafe_eraseunsafe_bucketunsafe_bucket_count、および unsafe_bucket_size という名前が付いています。 unsafe_ 名前付け規則では、これらのメソッドがコンカレンシーセーフではないことを示しています。 コンカレンシーセーフの詳細については、「コンカレンシーセーフ操作」をご覧ください。

  • 挿入操作では、既存のポインターまたは反復子を無効にしたり、マップ内に既に存在する項目の順序を変更したりすることはしません。 挿入操作と走査操作は同時に実行できます。

  • concurrent_unordered_map は前方反復のみをサポートしています。

  • 挿入は、equal_range によって返される反復子を無効にしたり、更新したりすることはありません。 挿入では、範囲の末尾に等しくない項目を追加できます。 begin 反復子が等しい項目を指しています。

デッドロックを回避するために、メモリ アロケーター、ハッシュ関数、またはその他のユーザー定義コードを呼び出すときに、concurrent_unordered_map にはロックを保持する方法はありません。 また、ハッシュ関数が同じ値に対して同じキーを常に評価する必要があります。 最適なハッシュ関数は、ハッシュ コード空間全体でキーを均等に分散します。

コンカレンシーセーフ操作

concurrent_unordered_map クラスを使用すると、コンカレンシーセーフな挿入操作と要素アクセス操作が可能になります。 挿入操作は、既存のポインターまたは反復子を無効にしません。 反復子アクセスと走査操作もコンカレンシーセーフです。 ここでは、コンカレンシー セーフは、ポインターまたは反復子が常に有効であることを意味します。 要素の初期化または特定の走査順序が保証されるわけではありません。 次の表は、コンカレンシーセーフな一般的に使用される concurrent_unordered_map メソッドと演算子を示しています。

count メソッドは同時に実行されるスレッドから安全に呼び出すことができますが、新しい値が同時にコンテナーに挿入されると、異なるスレッドが異なる結果を受け取ることがあります。

次の表は、コンカレンシーセーフではない一般的に使用されるメソッドと演算子を示しています。

これらのメソッドに加えて、unsafe_ で始まるメソッドもコンカレンシーセーフではありません。

[トップ]

concurrent_unordered_multimap クラス

concurrency::concurrent_unordered_multimap クラスは、複数の値を同じキーにマップできることを除けば、concurrent_unordered_map クラスによく似ています。 これは次の点で concurrent_unordered_map と異なります。

  • concurrent_unordered_multimap::insert メソッドは、std::pair<iterator, bool> の代わりに反復子を返します。

  • concurrent_unordered_multimap クラスは、operator[] メソッドも at メソッドも提供しません。

次の例は、concurrent_unordered_multimap を使用するための基本的な構造を示しています。 この例では、['a', 'i'] の範囲内に文字キーを挿入します。 concurrent_unordered_multimap を使用すると、キーは複数の値を持つことができます。

// unordered-multimap-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_map.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the map in parallel.

    concurrent_unordered_multimap<char, int> map; 

    parallel_for(0, 10, [&map](int i) {
        char key = 'a' + (i%9); // Geneate a key in the range [a,i].
        int value = i;          // Set the value to i.
        map.insert(make_pair(key, value));
    });

    // Print the elements in the map.
    for_each(begin(map), end(map), [](const pair<char, int>& pr) {
        wcout << L"[" << pr.first << L", " << pr.second << L"] ";
    });
}
/* Sample output:
    [e, 4] [i, 8] [a, 9] [a, 0] [c, 2] [g, 6] [f, 5] [b, 1] [d, 3] [h, 7]
*/

[トップ]

concurrent_unordered_set クラス

concurrency::concurrent_unordered_set クラスは、キーと値のペアの代わりに値を管理する点を除いて concurrent_unordered_map クラスによく似ています。 concurrent_unordered_set クラスは、operator[] メソッドも at メソッドも提供しません。

次の例は、concurrent_unordered_set を使用するための基本的な構造を示しています。 この例では、['a', 'i'] の範囲内に文字値を挿入します。 挿入を並列で実行するのは安全です。

// unordered-set-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_set.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the set in parallel.

    concurrent_unordered_set<char> set; 

    parallel_for(0, 10000, [&set](int i) {
        set.insert('a' + (i%9)); // Geneate a value in the range [a,i].
    });

    // Print the elements in the set.
    for_each(begin(set), end(set), [](char c) {
        wcout << L"[" << c << L"] ";
    });
}
/* Sample output:
    [e] [i] [a] [c] [g] [f] [b] [d] [h]
*/

[トップ]

concurrent_unordered_multiset クラス

concurrency::concurrent_unordered_multiset クラスは、重複する値が許可されている点を除いて、concurrent_unordered_set クラスによく似ています。 これは次の点で concurrent_unordered_set と異なります。

  • concurrent_unordered_multiset::insert メソッドは、std::pair<iterator, bool> の代わりに反復子を返します。

  • concurrent_unordered_multiset クラスは、operator[] メソッドも at メソッドも提供しません。

次の例は、concurrent_unordered_multiset を使用するための基本的な構造を示しています。 この例では、['a', 'i'] の範囲内に文字値を挿入します。 concurrent_unordered_multiset を使用すると、値の複数回出現が可能になります。

// unordered-set-structure.cpp
// compile with: /EHsc
#include <ppl.h>
#include <concurrent_unordered_set.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain() 
{
    //
    // Insert a number of items into the set in parallel.

    concurrent_unordered_multiset<char> set; 

    parallel_for(0, 40, [&set](int i) {
        set.insert('a' + (i%9)); // Geneate a value in the range [a,i].
    });

    // Print the elements in the set.
    for_each(begin(set), end(set), [](char c) {
        wcout << L"[" << c << L"] ";
    });
}
/* Sample output:
    [e] [e] [e] [e] [i] [i] [i] [i] [a] [a] [a] [a] [a] [c] [c] [c] [c] [c] [g] [g]
    [g] [g] [f] [f] [f] [f] [b] [b] [b] [b] [b] [d] [d] [d] [d] [d] [h] [h] [h] [h]
*/

[トップ]

combinable クラス

concurrency::combinable クラスには、再利用可能なスレッド ローカル ストレージが用意されており、詳細な計算を実行した後、その計算を最終結果にマージできます。 combinable オブジェクトは減少変数と考えることができます。

combinable クラスは、複数のスレッドまたはタスク間で共有されるリソースがあるときに便利です。 combinable クラスは、共有リソースへのロックフリー アクセスを提供することによって、共有状態を回避することができます。 したがって、このクラスは、複数のスレッドからの共有データへのアクセスを同期するために、ミューテックスなどの同期メカニズムを使用する代わりに使用できます。

メソッドと機能

次の表に、combinable クラスの重要なメソッドの一部を示します。 すべての combinable クラス メソッドの詳細については、「combinable クラス」をご覧ください。

メソッド 説明
local 現在のスレッド コンテキストに関連付けられているローカル変数への参照を取得します。
clear combinable オブジェクトからすべてのスレッドローカル変数を削除します。
combine

combine_each
指定された結合関数を使用して、すべてのスレッドローカル計算のセットから最終的な値を生成します。

combinable クラスは、最終的にマージされた結果でパラメーター化されるテンプレート クラスです。 既定のコンストラクターを呼び出す場合、T テンプレート パラメーターの型には、既定のコンストラクターとコピー コンストラクターが必要です。 T テンプレート パラメーターの型に既定のコンストラクターが存在しない場合は、初期化関数をパラメーターとして受け取るコンストラクターのオーバーロードされたバージョンを呼び出します。

combine メソッドまたは combine_each メソッドを呼び出した後、combinable オブジェクトに追加のデータ格納できます。 combine メソッドと combine_each メソッドを複数回呼び出す方法も可能です。 combinable オブジェクト内で変更されたローカル値がない場合、combine メソッドと combine_each メソッドは、呼び出されるごとに同じ結果を生成します。

combinable クラスの使用例については、次のトピックを参照してください。

[トップ]

方法: 並列コンテナーを使用して効率を向上させる
並列コンテナーを使用して、データの格納とアクセスを並行して効率的に行う方法について説明します。

方法: combinable を使用してパフォーマンスを向上させる
combinable クラスを使用して共有状態を回避し、それによってパフォーマンスを向上させる方法を示します。

方法: combinable を使用して集合を結合する
combine 関数を使用してスレッドローカルのデータ セットをマージする方法を示します。

並列パターン ライブラリ (PPL)
同時実行アプリケーションの開発に不可欠な、スケーラビリティが高く使いやすいプログラミング モデルを提供する PPL について説明します。

リファレンス

concurrent_vector クラス

concurrent_queue クラス

concurrent_unordered_map クラス

concurrent_unordered_multimap クラス

concurrent_unordered_set クラス

concurrent_unordered_multiset クラス

combinable クラス