一部のループでは、反復空間の最後が不明であるか、あるいはループ本体が、ループが終了する前に反復を追加することがあります。 どちらの場合も、tbb::parallel_do テンプレート・クラスを使用することで対処できます。
リンクリストは、終端が不明な反復空間の例です。 並列プログラミングでは、リンクリストの項目へのアクセスは本質的にシリアルであるため、リンクリストの代わりに動的配列を使用するほうが効率的です。 しかし、リンクリストを使用する必要があり、項目が安全に並列処理可能で、各項目の処理に少なくとも数千命令がかかる場合、parallel_do を使用して一部を並列処理します。
例えば、次のシリアルコードについて考えてみます。
void SerialApplyFooToList( const std::list<Item>& list ) {
for( std::list<Item>::const_iterator i=list.begin() i!=list.end(); ++i )
Foo(*i);
}
Foo の実行に少なくとも数千個の命令が費やされる場合、parallel_do を使用するようにループを変換することで、並列処理を高速化できます。 そのためには、const 修飾された operator() を使用してオブジェクトを定義します。 これは、operator() が const でなければならない点を除いて、C++ 標準ヘッダー <functional> の C++ 関数オブジェクトと似ています。
class ApplyFoo {
public:
void operator()( Item& item ) const {
Foo(item);
}
};
SerialApplyFooToList の並列形式は次のようになります。
void ParallelApplyFooToList( const std::list<Item>& list ) {
parallel_do( list.begin(), list.end(), ApplyFoo() );
}
parallel_do を呼び出しても、2 つのスレッドは同時に入力イテレーターで動作しません。 シリアルプログラムの入力イテレーターの定義は正しく動作します。 ワークのフェッチはシリアルであるため、parallel_do はスケーラブルでなくなります。 しかし、多くの状況では、シリアルに処理した場合よりも高速化されます。
parallel_do でワークをスケーラブルに取得する方法は 2 つあります。
イテレーターをランダムアクセス・イテレーターにします。
parallel_do の本体引数は、2 番目の引数が型 parallel_do<Item>& のフィーダー の場合、feeder.add(item) を呼び出すことで、より多くのワークを追加できます。 例えば、ツリーのノードを処理することは、その子孫を処理するための前提条件です。 parallel_do では、ノードを処理した後、feeder.add を使用して子孫ノードを追加できます。 parallel_do のインスタンスは、すべての項目が処理されるまで終了しません。
examples/parallel_do/parallel_preorder ディレクトリーには、parallel_do を使用して、非巡回有向グラフの並列の前順走査を行う小さなアプリケーションが含まれています。 このサンプルは、parallel_do_feeder を使用してより多くのワークを追加する方法を示しています。