このセクションでは、推奨するタスクパターンを説明します。各パターンで、T クラスは task クラスから派生すると仮定されます。サブタスクは、t1、t2、... tk と表記します。添字は、並列処理できない場合にサブタスクを実行する順序を示します。並列処理が可能な場合、サブタスクの実行順は、t1 が生成されるスレッドによって実行されることが保証される以外、実行の順番に再現性はありません。
再帰タスクパターンは、タスク・スケジューラーが潜在的な並列処理を明らかにして利用可能な並列処理にできるため、効率的でスケーラブルな並列処理に推奨されます。再帰タスクパターンは、ルートタスク t0 を作成して次のように実行することで始まります。
T& t0 = *new(allocate_root()) T(...); task::spawn_root_and_wait(t0);
ルートタスクの execute() メソッドは、この後に記述されているように多くのタスクを再帰的に作成します。
次のコードは、各レベルが k の子を生成する T 型の再帰タスクの推奨スタイルを示しています。
task* T::execute() { if( not recursing any further ) { ... } else { set_ref_count(k+1); task& tk = *new(allocate_child()) T(...); spawn(tk); task& tk-1= *new(allocate_child()) T(...); spawn(tk-1); ... task& t1= *new(allocate_child()) T(...); spawn_and_wait_for_all(t1); } return NULL; }
タスクの生成前にタスクが構築される場合、子の構築と生成の順序は変更できます。
このパターンの重要なポイントは次のとおりです。
set_ref_count の呼び出しはその引数として k+1 を使用します。追加の +1 は重要です。
タスクはそれぞれ allocate_child によって割り当てられます。
spawn_and_wait_for_all の呼び出しは、作成と待機を組み合わせます。より統一されているが多少効率が落ちる別の方法として、spawn ですべてのタスクを作成し、wait_for_all を呼び出して待機する方法があります。
2 つの推奨するスタイルがあります。これらのスタイルは、継続して親を再利用するほうが便利なのか、子として親を再利用するほうが便利なのかという点で異なります。継続と子のどちらがより親のように働くか、という判断に基づいて決定します。
次の例では、オプションで、子を作成する代わりに子の 1 つのポインターを返すことができます。ポインターを返すようにした場合、親がリターンした直後に子が実行されます。タスクプールにタスクを格納して取り出す無意味なオーバーヘッドをスキップできるため、このオプションを選択すると効率が向上することがあります。
継続が親の状態の多くを継承する必要があり、子が状態を必要としない場合、このスタイルが便利です。継続は親と同じ型を持っていなければなりません。
task* T::execute() { if( not recursing any further ) { ... return NULL; } else { set_ref_count(k); recycle_as_continuation(); task& tk = *new(allocate_child()) T(...); spawn(tk); task& tk-1 = *new(allocate_child()) T(...); spawn(tk-1); ... // 不必要なオーバーヘッドを削除するため、タスクを // 作成する代わりに最初の子のポインターを返す task& t1 = *new(allocate_child()) T(...); return &t1; } }
このパターンの重要なポイントは次のとおりです。
set_ref_count の呼び出しはその引数として k を使用します。ブロックスタイルであった追加の +1 はありません。
子タスクはそれぞれ allocate_child によって割り当てられます。
継続は親を再利用し、コピー操作を行わずに親の状態を取得します。
子が親からのその状態の多くを継承し、継続が親の状態を必要としない場合、このスタイルが便利です。子は親の型を持っていなければなりません。例では、C は継続の型で、task クラスから派生します。C が、すべての子が完了するのを待つ以外に何もしない場合、C は empty_task クラスです。
task* T::execute() { if( not recursing any further ) { ... return NULL; } else { // 継続を構築 C& c = allocate_continuation(); c.set_ref_count(k); // 最初の子として自身を再利用 task& tk = *new(c.allocate_child()) T(...); spawn(tk); task& tk-1 = *new(c.allocate_child()) T(...); spawn(tk-1); ... task& t2 = *new(c.allocate_child()) T(...); spawn(t2); // タスク t1 は再利用された自身 recycle_as_child_of(c); t1 で解決される下位の問題のために *this のフィールドを更新 return this; } }
このパターンの重要なポイントは次のとおりです。
set_ref_count の呼び出しはその引数として k を使用します。ブロックスタイルであった追加の +1 はありません。
t1 を除く子タスクはそれぞれ c.allocate_child によって割り当てられます。(*this).allocate_child ではなく c.allocate_child を使用することが重要です。そうしないと、タスクグラフに誤りが生じます。
タスク t1 は親から再利用され、コピー操作を行わずに親の状態を取得します。子を表すように状態を更新することを忘れないでください。そうしないと、無限の再帰が発生します。
子タスクの実行中にメインスレッドの実行を続けることが望ましい場合があります。次の例は、ダミータスク empty_task を使用してこの処理を行っています。
task* dummy = new( task::allocate_root() ) empty_task; dummy->set_ref_count(k+1); task& tk = *new( dummy->allocate_child() ) T; dummy->spawn(tk); task& tk-1 = *new( dummy->allocate_child() ) T; dummy->spawn(tk-1); ... task& t1 = *new( dummy->allocate_child() ) T; dummy->spawn(t1); ...do any other work... dummy->wait_for_all(); dummy->destroy(*dummy);
このパターンの重要なポイントは次のとおりです。