タスクの並列化 (同時実行ランタイム)
更新 : 2011 年 3 月
ここでは、同時実行ランタイムでのタスクおよびタスク グループの役割について説明します。 同時に実行する独立した作業項目が複数ある場合は、タスク グループを使用します。 たとえば、残存作業を 2 つのパーティションに分割する再帰的なアルゴリズムがあるとします。 タスク グループを使用すると、これらのパーティションを同時に実行できます。 一方、同じルーチンをコレクションの各要素に並列に適用する場合は、Concurrency::parallel_for などの並列アルゴリズムを使用します。 並列アルゴリズムの詳細については、「並列アルゴリズム」を参照してください。
タスクとタスク グループ
タスクは、特定のジョブを実行する作業の単位です。 通常、タスクは他のタスクと並列に実行したり、より細かい別のタスクに分割したりすることができます。 タスク グループを使用して、タスクのコレクションを編成します。 タスク グループでは、ワーク スティーリング キューにタスクを置きます。 スケジューラはこのキューからタスクを削除し、使用できるコンピューティング リソースでそのタスクを実行します。 タスク グループにタスクを追加した場合、すべてのタスクが終了するまで待機することも、まだ開始されていないタスクを取り消すこともできます。
PPL では、Concurrency::task_group および Concurrency::structured_task_group クラスを使用してタスク グループを表し、Concurrency::task_handle クラスを使用してタスクを表します。 task_handle クラスは、処理を行うコードをカプセル化します。 このコードは、ラムダ関数、関数ポインター、または関数オブジェクトの形式で使用され、多くの場合、処理関数と呼ばれます。 通常、task_handle オブジェクトを直接操作する必要はありません。 代わりに、タスク グループに処理関数を渡します。タスク グループによって task_handle オブジェクトが作成および管理されます。
タスク グループは非構造化タスク グループおよび構造化タスク グループという 2 つのカテゴリに分類されます。 PPL では、task_group クラスを使用して非構造化タスク グループを表し、structured_task_group クラスを使用して構造化タスク グループを表します。
重要
また、PPL では、structured_task_group クラスを使用して一連のタスクを並列に実行する Concurrency::parallel_invoke アルゴリズムも定義します。 parallel_invoke アルゴリズムにはより簡潔な構文が用意されているため、可能であれば structured_task_group クラスの代わりに使用することをお勧めします。 parallel_invoke の詳細については、「並列アルゴリズム」を参照してください。
parallel_invoke は、同時に実行する独立したタスクが複数あり、すべてのタスクが終了するまで待機してから処理を続行する必要がある場合に使用します。 task_group は、同時に実行する独立したタスクが複数あり、それらのタスクが終了するタイミングがまだ先である場合に使用します。 たとえば、task_group オブジェクトにタスクを追加して、それらのタスクが別の関数や別のストレッドで終了するまで待機できます。
タスク グループでは、キャンセル処理の概念がサポートされています。 キャンセル処理を使用すると、操作全体を取り消すことをアクティブなすべてのタスクに通知できます。 また、キャンセル処理により、まだ開始されていないタスクが実行されるのを防止できます。 キャンセル処理の詳細については、「PPL における取り消し処理」を参照してください。
また、ランタイムでは、例外処理モデルを使用することによって、タスクから例外をスローし、関連するタスク グループが終了するまで待機しているときにその例外を処理できます。 この例外処理モデルの詳細については、「同時実行ランタイムでの例外処理」を参照してください。
task_group と structured_task_group の違い
structured_task_group クラスの代わりに task_group または parallel_invoke を使用することが推奨されますが、可変個のタスクを実行する並列アルゴリズムやキャンセル処理のサポートが必要な並列アルゴリズムを記述する場合など、structured_task_group を使用した方がよい場合もあります。 ここでは、task_group クラスと structured_task_group クラスの違いについて説明します。
task_group クラスはスレッド セーフです。 したがって、複数のスレッドから task_group オブジェクトにタスクを追加したり、複数のスレッドから task_group オブジェクトに対して待機や取り消しの操作を行ったりしてもかまいません。 structured_task_group オブジェクトの構築と破棄は、同じ構文のスコープで行う必要があります。 また、structured_task_group オブジェクトに対する操作はすべて同じスレッドで行う必要があります。 この規則の例外は、Concurrency::structured_task_group::cancel メソッドと Concurrency::structured_task_group::is_canceling メソッドです。 子タスクでこれらのメソッドを呼び出すことで、任意のタイミングで親タスク グループを取り消したり、取り消し処理をチェックしたりできます。
Concurrency::task_group::wait メソッドまたは Concurrency::task_group::run_and_wait メソッドの呼び出し後、task_group オブジェクトに対しては別のタスクを実行できます。 一方、Concurrency::structured_task_group::wait メソッドまたは Concurrency:: structured_task_group::run_and_wait メソッドの呼び出し後、structured_task_group オブジェクトに対して別のタスクを実行することはできません。
structured_task_group クラスはスレッド間で同期されないため、実行に伴うオーバーヘッドが task_group クラスよりも少なくなります。 したがって、複数のスレッドから処理をスケジュールする必要のない問題に対処する場合に、parallel_invoke アルゴリズムを使用できないときは、structured_task_group クラスを使用すると、よりパフォーマンスの高いコードを作成できます。
structured_task_group オブジェクトを別の structured_task_group オブジェクト内で使用する場合は、外部オブジェクトが終了する前に内部オブジェクトが終了して破棄される必要があります。 task_group クラスの場合、外部グループが終了する前に、入れ子になったタスク グループが終了する必要はありません。
非構造化タスク グループと構造化タスク グループでは、さまざまな方法でタスク ハンドルを操作します。 task_group オブジェクトには処理関数を直接渡すことができます。task_group オブジェクトによってタスク ハンドルが自動的に作成および管理されます。 structured_task_group クラスを使用する場合は、タスクごとに task_handle オブジェクトを管理する必要があります。 各 task_handle オブジェクトは、関連する structured_task_group オブジェクトの有効期間を通じて有効である必要があります。 次の基本的な例に示すように、Concurrency::make_task 関数を使用して task_handle オブジェクトを作成します。
// make-task-structure.cpp
// compile with: /EHsc
#include <ppl.h>
using namespace Concurrency;
int wmain()
{
// Use the make_task function to define several tasks.
auto task1 = make_task([] { /*TODO: Define the task body.*/ });
auto task2 = make_task([] { /*TODO: Define the task body.*/ });
auto task3 = make_task([] { /*TODO: Define the task body.*/ });
// Create a structured task group and run the tasks concurrently.
structured_task_group tasks;
tasks.run(task1);
tasks.run(task2);
tasks.run_and_wait(task3);
}
可変個のタスクが存在する状況でタスク ハンドルを管理するには、_malloca などのスタック割り当てルーチンを使用するか、std::vector などのコンテナー クラスを使用します。
task_group と structured_task_group の両方でキャンセル処理がサポートされています。 キャンセル処理の詳細については、「PPL における取り消し処理」を参照してください。
例
次の簡単な例では、タスク グループの操作方法を示します。 この例では、parallel_invoke アルゴリズムを使用して、2 つのタスクを同時に実行します。 各タスクでは、サブタスクを task_group オブジェクトに追加します。 task_group クラスを使用した場合、複数のタスクでタスクを同時に追加できることに注意してください。
// using-task-groups.cpp
// compile with: /EHsc
#include <ppl.h>
#include <sstream>
#include <iostream>
using namespace Concurrency;
using namespace std;
// Prints a message to the console.
template<typename T>
void print_message(T t)
{
wstringstream ss;
ss << L"Message from task: " << t << endl;
wcout << ss.str();
}
int wmain()
{
// A task_group object that can be used from multiple threads.
task_group tasks;
// Concurrently add several tasks to the task_group object.
parallel_invoke(
[&] {
// Add a few tasks to the task_group object.
tasks.run([] { print_message(L"Hello"); });
tasks.run([] { print_message(42); });
},
[&] {
// Add one additional task to the task_group object.
tasks.run([] { print_message(3.14); });
}
);
// Wait for all tasks to finish.
tasks.wait();
}
この例のサンプル出力を次に示します。
Message from task: Hello
Message from task: 3.14
Message from task: 42
parallel_invoke アルゴリズムではタスクを同時に実行するため、出力メッセージの順序が変わる可能性があります。
parallel_invoke アルゴリズムの使用方法を示す完全な例については、「方法: 並列呼び出しを使用して並列並べ替えルーチンを記述する」および「方法: 並列呼び出しを使用して並列操作を実行する」を参照してください。 task_group クラスを使用して非同期フューチャを実装する方法を示す完全な例については、「チュートリアル: フューチャの実装」を参照してください。
信頼性の高いプログラミング
タスク グループと並列アルゴリズムを使用する場合は、キャンセル処理と例外処理の役割を十分に理解しておいてください。 たとえば、並列処理ツリーでタスクを取り消すと、子タスクも実行されなくなります。 そのため、アプリケーションで重要となる操作 (リソースの解放など) が子タスクのいずれかで実行されるような場合に問題となります。 また、子タスクが例外をスローすると、その例外がオブジェクトのデストラクターを介して反映され、アプリケーションで未定義の動作が発生する可能性があります。 これらの点を示す例については、「並列パターン ライブラリに関するベスト プラクティス」の「取り消し処理および例外処理がオブジェクトの破棄に及ぼす影響について」を参照してください。 PPL でのキャンセル モデルと例外処理モデルの詳細については、「PPL における取り消し処理」および「同時実行ランタイムでの例外処理」を参照してください。
関連トピック
方法: 並列呼び出しを使用して並列並べ替えルーチンを記述する
parallel_invoke アルゴリズムを使用して、バイトニック ソート アルゴリズムのパフォーマンスを向上させる方法について説明します。方法: 並列呼び出しを使用して並列操作を実行する
parallel_invoke アルゴリズムを使用して、共有データ ソースに対して複数の操作を実行するプログラムのパフォーマンスを向上させる方法について説明します。チュートリアル: フューチャの実装
同時実行ランタイムの既存の機能を組み合わせて、より効果的に使用する方法を示します。並列パターン ライブラリ (PPL)
同時実行アプリケーションの開発に不可欠なプログラミング モデルを提供する PPL について説明します。
参照
履歴の変更
日付 |
履歴 |
理由 |
---|---|---|
2011 年 3 月 |
タスク グループと並列アルゴリズムを使用する場合のキャンセル処理と例外処理の役割に関する情報を追加。 |
情報の拡充 |
2010 年 7 月 |
内容を再構成。 |
情報の拡充 |
2010 年 5 月 |
ガイドラインを拡張。 |
情報の拡充 |