Share via


タスクの並列化 (同時実行ランタイム)

ここでは、同時実行ランタイムでのタスクおよびタスク グループの役割について説明します。タスクは、特定のジョブを実行する作業の単位です。タスクは他のタスクと並列に実行したり、追加のタスクにより粒度の細かい、分割したりすることができます。タスク グループを使用して、タスクのコレクションを編成します。

非同期操作が完了したら非同期コードを記述、操作を実行するときにタスクを使用します。たとえば、使用できる状態になったら、非同期的にファイルから読み取るタスクとデータを処理するこのドキュメントで後述する継続タスクを使用する場合があります。逆に、小さい部分に並列処理を分解使用するタスク グループ。たとえば、残存作業を 2 つのパーティションに分割する再帰的なアルゴリズムがあるとします。これらのパーティションを同時に実行するタスク グループを使用し、次に完了するように分割処理を待機します。

ヒントヒント

コレクションの各要素に同じルーチンを並列に適用する場合は、タスクまたはタスク グループではなく、並列アルゴリズムを concurrency::parallel_forなど) を使用します。並列アルゴリズムの詳細については、「並列アルゴリズム」を参照してください。

キー

  • ラムダ式に変数を渡すときに参照渡しを使用する場合、タスクが終了するまでその変数の有効期間が続くようにする必要があります。

  • 非同期コードを記述するときに タスクconcurrency::task (クラス) を使用します。

  • 並列処理をより小さい部分に分解し、完了するためにこれらの小さい部分を待機する必要があるときにタスク グループを使用します (concurrency::task_group のクラスまたは concurrency::parallel_invoke のアルゴリズムなど)。

  • 継続の作成に concurrency::task::then のメソッドを使用します。継続は 別のタスクの後の実行を非同期的に実行するタスクです。非同期処理を形成するために継続をいくつでも接続できます。

  • タスク ベースの継続は、継続元タスクが取り消されたか、または例外をスローする場合でも継続元タスクが終了するときに常に実装されます。

  • 一連のタスクのすべてのメンバーが完了すると完了したタスクの作成に concurrency::when_all を使用します。一連のタスクの 1 個のメンバーが完了すると完了したタスクの作成に concurrency::when_any を使用します。

  • タスクとタスク グループは、PPL でのキャンセル機構に参加できます。詳細については、「PPL における取り消し処理」を参照してください。

  • ランタイムがタスクおよびタスク グループによってスローされた例外を処理する方法については、同時実行ランタイムでの例外処理を参照してください。

このドキュメント

  • ラムダ式の使用

  • タスク クラス

  • 継続タスク

  • 継続タスク ベースのベースと値

  • 構成タスク

    • when_all 関数

    • 関数 when_any

  • 遅延タスクの実行

  • タスク グループ

  • task_group と structured_task_group の違い

  • 信頼性の高いプログラミング

ラムダ式の使用

ラムダ式は簡潔な構文に対してタスクおよびタスク グループ実行される作業を定義する一般的な方法です。これらの使用のヒントを次に示します。:

  • タスクがバックグラウンド スレッドでよく実行されるため、ラムダ式の変数をキャプチャするオブジェクトの有効期間に注意してください。値が変数をキャプチャすると、その変数のコピーは、ラムダの本体で作成されます。参照によってキャプチャすると、コピーは作成されません。したがって、参照によってキャプチャするすべての変数の有効期間がタスクを使用する克服することを確認します。

  • 一般に、それによって変数が割り当てられますスタックでキャプチャしません。これは、スタックに割り当てられるオブジェクトのメンバー変数をキャプチャする必要があることを意味します。

  • 参照によって値としてキャプチャしているスレッドを識別するために、ラムダ式でキャプチャ変数に明示です。したがって、ラムダ式に [=] または [&] オプションを使用することはお勧めしません。

1 個の一般的なパターンは変数への継続のチェーンの割り当ての 1 個のタスクと、個別のタスクがその変数を読み取る場合です。各継続タスクがその変数のコピーを保持するため、値によってキャプチャすることはできません。スタック割り当て変数が有効かがあるため、参照によってキャプチャすることはできません。

この問題を解決するには、変数をラップするには、スマート ポインターを、std::shared_ptrなど) を使用し、スマート ポインターの値を渡します。それにより、基になるオブジェクトがに割り当てられ、から読み込むことができ、それを使用してタスクを克服します。変数が Windows のランタイム オブジェクトへのポインター、参照カウント ハンドル (^) であっても、この方法を使用します。基本的な例を次に示します。:

// lambda-task-lifetime.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
#include <string>

using namespace concurrency;
using namespace std;

task<wstring> write_to_string()
{
    // Create a shared pointer to a string that is 
    // assigned to and read by multiple tasks.
    // By using a shared pointer, the string outlives
    // the tasks, which can run in the background after
    // this function exits.
    auto s = make_shared<wstring>(L"Value 1");

    return create_task([s] 
    {
        // Print the current value.
        wcout << L"Current value: " << *s << endl;
        // Assign to a new value.
        *s = L"Value 2";

    }).then([s] 
    {
        // Print the current value.
        wcout << L"Current value: " << *s << endl;
        // Assign to a new value and return the string.
        *s = L"Value 3";
        return *s;
    });
}

int wmain()
{
    // Create a chain of tasks that work with a string.
    auto t = write_to_string();

    // Wait for the tasks to finish and print the result.
    wcout << L"Final value: " << t.get() << endl;
}

/* Output:
    Current value: Value 1
    Current value: Value 2
    Final value: Value 3
*/

ラムダ式の詳細については、C++ でのラムダ式を参照してください。

[上]

タスク クラス

一連のタスクに依存する操作を構成するに concurrency::task のクラスを使用できます。この構成モデルは、継続の概念によってサポートされます。継続は、継続元の前か、タスクの完了時に実行するコードを有効にします。継続元タスクの結果は一つ以上の継続タスクへ入力として渡されます。継続元タスクが完了すると、で待機しているすべての継続タスクに有効になります。各継続タスクは継続元タスク結果のコピーを受信します。次に、これらの継続タスクが、これにより、タスクのチェーンを作成する他の継続の継続元タスクである場合があります。継続は間の特定の依存関係があるタスクは、任意の長さのチェーンを作成できます。また、タスクがキャンセル前に、タスクの開始日または協調的に実行中に参加できます。このキャンセル モデルの詳細については、PPL における取り消し処理を参照してください。

task は、テンプレート クラスです。型パラメーターは T タスクで生成される結果の型です。この型は、タスクが値を返さない void です。T は const 修飾子を使用できません。

タスクを作成すると、タスクの本体を実行する 処理関数を 提供します。この処理関数は、ラムダ関数、関数ポインター、または関数オブジェクトの形式に使用されます。結果を派生させず、タスクの完了を待つためには concurrency::task::wait のメソッドを呼び出します。task::wait のメソッドは、タスクが完了するか、取り消されたかどうかを示す concurrency::task_status の値を返します。タスクの結果を取得するには、concurrency::task::get のメソッドを呼び出します。このメソッドは、結果が使用可能になるまでタスクの完了を待つために task::wait を呼び出して、現在のスレッドの実行をブロックします。

次の例では、タスクを作成し、結果を待機し、値を表示する方法を示します。このドキュメントの例は、より簡潔な構文が用意されているため、ラムダ関数を使用します。ただし、タスクを使用すると、関数ポインター、および関数オブジェクトを使用できます。

// basic-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Create a task.
    task<int> t([]()
    {
        return 42;
    });

    // In this example, you don't necessarily need to call wait() because
    // the call to get() also waits for the result.
    t.wait();

    // Print the result.
    wcout << t.get() << endl;
}

/* Output:
    42
*/

concurrency::create_task の関数は、型を宣言する代わりに auto のキーワードを使用することができます。たとえば、単位行列を作成し、印刷する次のコードを検討する:

// create-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <string>
#include <iostream>
#include <array>

using namespace concurrency;
using namespace std;

int wmain()
{
    task<array<array<int, 10>, 10>> create_identity_matrix([]
    {
        array<array<int, 10>, 10> matrix;
        int row = 0;
        for_each(begin(matrix), end(matrix), [&row](array<int, 10>& matrixRow) 
        {
            fill(begin(matrixRow), end(matrixRow), 0);
            matrixRow[row] = 1;
            row++;
        });
        return matrix;
    });

    auto print_matrix = create_identity_matrix.then([](array<array<int, 10>, 10> matrix)
    {
        for_each(begin(matrix), end(matrix), [](array<int, 10>& matrixRow) 
        {
            wstring comma;
            for_each(begin(matrixRow), end(matrixRow), [&comma](int n) 
            {
                wcout << comma << n;
                comma = L", ";
            });
            wcout << endl;
        });
    });

    print_matrix.wait();
}
/* Output:
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0
    0, 1, 0, 0, 0, 0, 0, 0, 0, 0
    0, 0, 1, 0, 0, 0, 0, 0, 0, 0
    0, 0, 0, 1, 0, 0, 0, 0, 0, 0
    0, 0, 0, 0, 1, 0, 0, 0, 0, 0
    0, 0, 0, 0, 0, 1, 0, 0, 0, 0
    0, 0, 0, 0, 0, 0, 1, 0, 0, 0
    0, 0, 0, 0, 0, 0, 0, 1, 0, 0
    0, 0, 0, 0, 0, 0, 0, 0, 1, 0
    0, 0, 0, 0, 0, 0, 0, 0, 0, 1
*/

同等の操作を作成するに create_task 関数を使用できます。

auto create_identity_matrix = create_task([]
{
    array<array<int, 10>, 10> matrix;
    int row = 0;
    for_each(begin(matrix), end(matrix), [&row](array<int, 10>& matrixRow) 
    {
        fill(begin(matrixRow), end(matrixRow), 0);
        matrixRow[row] = 1;
        row++;
    });
    return matrix;
});

例外がタスクの実行時にスローされた場合、ランタイムのマーシャリング task::get または task::wait、またはタスク ベースの継続への以降の呼び出しの例外。タスクの例外処理機構に関する詳細については、同時実行ランタイムでの例外処理を参照してください。

taskconcurrency::task_completion_eventの使用例については、キャンセルは、チュートリアル: タスクおよび XML HTTP 要求を使用した接続 (IXHR2)が表示されます。(task_completion_event のクラスは、このドキュメントで後に説明します)。

ヒントヒント

Windows ストア の apps のタスクに固有の詳細を学習するには、Asynchronous programming in C++C++ における Windows ストア アプリ用の非同期操作の作成を参照してください。

[上]

継続タスク

非同期プログラミングでは、非同期操作で完了時に 2 番目の操作を呼び出してデータを渡すのが一般的です。従来、これはコールバック メソッドを使用してされます。同時実行ランタイムでは、継続タスクに同じ機能が用意されています。継続タスク (単に "継続" とも呼ばれます) とは、別のタスク ("継続元" と呼ばれます) が完了したときにそのタスクによって呼び出される非同期タスクのことです。継続を使用して、:

  • 継続元から継続にデータを渡します。

  • 継続が開始または呼び出されない正確な条件を指定します。

  • 実行中または開始される前、またはに継続を取り消すします。

  • 継続の動作に関するスケジュールするかヒントを提供します。(これは Windows ストア に apps のみを追加します。詳細については、「C++ における Windows ストア アプリ用の非同期操作の作成」を参照してください。

  • 同じ継続元から複数の継続を開始します。

  • 1 種類の継続をときに複数の継続元のすべてまたは起動します。

  • 任意の長さに継続を実行するチェーンであるします。

  • 継続元によってスローされた例外を処理するために継続を使用します。

これらの機能は、最初のタスクが完了したときに一つ以上のタスクを実行できるようにします。たとえば、最初のタスクがディスクからデータを読み取るとファイルを圧縮して継続を作成できます。

次の例では、使用可能な場合、前の継続元タスクの値を出力して継続をスケジュールする concurrency::task::then メソッドを使用するように 1 "に変更されます。

// basic-continuation.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]() -> int
    {
        return 42;
    });

    t.then([](int result)
    {
        wcout << result << endl;
    }).wait();

    // Alternatively, you can chain the tasks directly and
    // eliminate the local variable.
    /*create_task([]() -> int
    {
        return 42;
    }).then([](int result)
    {
        wcout << result << endl;
    }).wait();*/
}

/* Output:
    42
*/

任意の長さにタスクをチェーンしたり、入れ子にできます。タスクは、複数の継続を持つことができます。次の例では、3 回前のタスクの値をインクリメントする基本的な継続のチェーンを示しています。

// continuation-chain.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]() -> int
    { 
        return 0;
    });

    // Create a lambda that increments its input value.
    auto increment = [](int n) { return n + 1; };

    // Run a chain of continuations and print the result.
    int result = t.then(increment).then(increment).then(increment).get();
    wcout << result << endl;
}

/* Output:
    3
*/

継続は、別のタスクを返すことができます。キャンセルがない場合、このタスクは後続の継続の前に実行されます。この方法は 、非同期開くことと呼ばれます。開く非同期は、追加の作業を実行する必要があるときに現在のスレッドをブロック現在のタスクが必要ない場合に役立ちます。(これは、継続が UI スレッドで実行できる) Windows ストア の apps で共通です。次の例では、3 種類のタスクを示しています。最初のタスクは継続タスク実行前に別のタスクを返します。

// async-unwrapping.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]()
    {
        wcout << L"Task A" << endl;

        // Create an inner task that runs before any continuation
        // of the outer task.
        return create_task([]()
        {
            wcout << L"Task B" << endl;
        });
    });

    // Run and wait for a continuation of the outer task.
    t.then([]()
    {
        wcout << L"Task C" << endl;
    }).wait();
}

/* Output:
    Task A
    Task B
    Task C
*/
重要 : 重要

タスクの継続が N型の入れ子のタスクを返す場合、入れ子のタスクが完了すると、タスクに型 N、task<N>があり、終了します。つまり、入れ子のタスクの継続は開くことを実行します。

[上]

継続タスク ベースのベースと値

戻り値の型が Tである task のオブジェクトは、継続タスクに型 Ttask<T> かの値を指定できます。型 T を受け取る継続は 値ベースの継続と呼ばれます。値ベースの継続は、継続元タスクがエラーなしで完了したときに適用され、取り消されません。パラメーターは タスク ベースの継続と呼ばれるように型を受け取る task<T> 継続。タスク ベースの継続は、継続元タスクが取り消されたか、または例外をスローする場合でも継続元タスクが終了するときに常に実装されます。次に、継続元タスクの結果を得るには task::get を呼び出すことができます。継続元タスクが取り消された場合、task::getconcurrency::task_canceledをスローします。継続元タスクが例外をスローすると、task::get の例外では。タスク ベースの継続はキャンセルを継続元タスクがキャンセルされるとマークされていません。

[上]

構成タスク

ここでは、一般的なパターンを実装するいくつかのタスクを構成するために役立つ concurrency::when_allconcurrency::when_any 関数について説明します。

Dd492427.collapse_all(ja-jp,VS.110).gifwhen_all 関数

when_all 関数は、完全な一連のタスクの後にタスクが完了するのを生成します。この関数は、セット内の各タスクの結果を含む std::vector のオブジェクトを返します。次の基本的な例は 3 つが他のタスクの完了を表すタスクの作成に when_all を使用します。

// join-tasks.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Start multiple tasks.
    array<task<void>, 3> tasks = 
    {
        create_task([] { wcout << L"Hello from taskA." << endl; }),
        create_task([] { wcout << L"Hello from taskB." << endl; }),
        create_task([] { wcout << L"Hello from taskC." << endl; })
    };

    auto joinTask = when_all(begin(tasks), end(tasks));

    // Print a message from the joining thread.
    wcout << L"Hello from the joining thread." << endl;

    // Wait for the tasks to finish.
    joinTask.wait();
}

/* Sample output:
    Hello from the joining thread.
    Hello from taskA.
    Hello from taskC.
    Hello from taskB.
*/

[!メモ]

when_all に渡すタスクは均一である必要があります。つまり、これらはすべて同じ型を返す必要があります。

フル セットのタスクの後に実行する次の例に示すように、タスクを生成するに && の構文を使用できます。

auto t = t1 && t2; // same as when_all

これは、一連のタスクが終了すると共通処理を実行する when_all とともに継続を使用するようになります。次の例は、前の 3 種類のタスクの合計を印刷するために 1 "に変更されますが int それぞれの結果が生成される。

// Start multiple tasks.
array<task<int>, 3> tasks =
{
    create_task([]() -> int { return 88; }),
    create_task([]() -> int { return 42; }),
    create_task([]() -> int { return 99; })
};

auto joinTask = when_all(begin(tasks), end(tasks)).then([](vector<int> results)
{
    wcout << L"The sum is " 
          << accumulate(begin(results), end(results), 0)
          << L'.' << endl;
});

// Print a message from the joining thread.
wcout << L"Hello from the joining thread." << endl;

// Wait for the tasks to finish.
joinTask.wait();

/* Output:
    Hello from the joining thread.
    The sum is 229.
*/

この例では、タスク ベースの継続を作成するために task<vector<int>> を指定できます。

Caution メモ注意

一連のタスクのすべてのタスクが取り消されたか、または例外をスローした場合、when_all は、残りのタスクを完了し、完了まで待機しません。例外がスローされた場合、when_all が返す task のオブジェクトの task::gettask::wait を呼び出すと、ランタイムでの例外。複数のタスクからスローすると、ランタイムはその 1 を選択します。したがって、1 は例外をスローすると、完了するすべてのタスクを待機することを確認します。

[上]

Dd492427.collapse_all(ja-jp,VS.110).gif関数 when_any

when_any 関数は、一連のタスクの最初のタスクが完了すると、タスクの完了を生成します。この関数は、セットでそのタスクの完了したタスクとインデックスの結果を含む std::pair のオブジェクトを返します。

when_any 関数は次のような場合に特に便利です:

  • 追加の操作。多くの方法で実行できる操作またはアルゴリズムを検討してください。最初に終了が残りの操作をキャンセル操作を選択するに when_any 関数を使用できます。

  • インタリーブ操作。各操作の完了と同時に結果を処理するために、すべてが when_any 関数を解除、使用する複数の操作を開始できます。1 回の操作が完了したら、一つ以上の追加タスクを開始できます。

  • 絞られた操作。同時実行の数を制限して、前のシナリオを拡張するために when_any 関数を使用できます。

  • 有効期限切れの操作。一つ以上のタスクと、特定の時間後にタスクを終了するかを選択するために when_any 関数を使用できます。

when_allと同様にアクションを一連のタスクの 1 番目の実行が完了すると when_any がある継続を使用するには、のが一般的です。次の基本的な例は 3 つが他のタスクの先頭が完了すると完了したタスクの作成に when_any を使用します。

// select-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Start multiple tasks.
    array<task<int>, 3> tasks = {
        create_task([]() -> int { return 88; }),
        create_task([]() -> int { return 42; }),
        create_task([]() -> int { return 99; })
    };

    // Select the first to finish.
    when_any(begin(tasks), end(tasks)).then([](pair<int, size_t> result)
    {
        wcout << "First task to finish returns "
              << result.first
              << L" and has index "
              << result.second
              << L'.' << endl;
    }).wait();
}

/* Sample output:
    First task to finish returns 42 and has index 1.
*/

この例では、タスク ベースの継続を作成するために task<pair<int, size_t>> を指定できます。

[!メモ]

when_allと同様に、when_any に渡すタスクはすべて同じ型を返す必要があります。

また、次の例に示すように、一連のタスクの最初のタスクの完了した後で、タスクの完了を生成するに || の構文を使用できます。

auto t = t1 || t2; // same as when_any

[上]

遅延タスクの実行

条件が満たされた低下は、または外部イベントに応答してタスクがまでタスクの実行を開始することが必要な場合。たとえば、非同期プログラミングでは、I/O 完了のイベントに応答してタスクを起動する必要があります。

これを実現する 2 とおりの方法は、継続を使用するか、タスクの処理関数の中のイベントのタスクを待機を呼び出すことです。ただし、可能でないこれらの方法の 1 つがを使用するとなる場合もあります。たとえば、継続を作成するために、継続元タスクが必要です。ただし、継続元タスクが、使用できるようになると継続元タスクに対して タスクの完了イベント 以降のチェーンをその完了イベント作成できます。また、待機中のタスクは、スレッドをブロックするため、非同期操作で作業を行うためにタスクの完了イベントを使用し、完了スレッドが解放されます。

concurrency::task_completion_event のクラスで、タスクのこのような構成を簡素化できます。task のクラスと同様に、型パラメーターは T タスクで生成される結果の型です。この型は、タスクが値を返さない void です。T は const 修飾子を使用できません。通常、task_completion_event のオブジェクトは、を通知するタスクまたはスレッドに、の値が使用可能になると、提供されます。同時に、一つ以上のタスクは、そのイベントのリスナーとして設定されます。イベントを設定すると、リスナーは完全にタスク、継続が実行されるようにスケジュールされます。

遅延が、方法: 遅延後に完了するタスクを作成するを見た後に実行するタスクを実行するために task_completion_event の使用例について。

[上]

タスク グループ

タスク グループを使用して、タスクのコレクションを編成します。タスク グループでは、ワーク スティーリング キューにタスクを置きます。スケジューラはこのキューからタスクを削除し、使用できるコンピューティング リソースでそのタスクを実行します。タスク グループにタスクを追加した場合、すべてのタスクが終了するまで待機することも、まだ開始されていないタスクを取り消すこともできます。

PPL では、これらのグループで実行されるタスクを表すためにタスク グループを表すために concurrency::task_groupconcurrency::structured_task_group のクラスおよび concurrency::task_handle クラスを使用します。task_handle クラスは、処理を行うコードをカプセル化します。task のクラスと同様に、処理関数は、ラムダ関数の形式にても、ポインター、または関数オブジェクトを返します。通常、task_handle オブジェクトを直接操作する必要はありません。代わりに、タスク グループに処理関数を渡します。タスク グループによって task_handle オブジェクトが作成および管理されます。

タスク グループは非構造化タスク グループおよび構造化タスク グループという 2 つのカテゴリに分類されます。PPL では、非構造化タスク グループと構造化タスク グループを表すために structured_task_group のクラスを表すために 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_groupparallel_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::cancelconcurrency::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 オブジェクトの有効期間を通じて有効である必要があります。次の基本的な例に示すように task_handle のオブジェクトを作成するために concurrency::make_task 関数の使用:

// 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_groupstructured_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 クラスを使用して非同期フューチャを実装する方法を示す完全な例については、「チュートリアル: フューチャの実装」を参照してください。

[上]

信頼性の高いプログラミング

タスク、タスク グループと並列アルゴリズムを使用する場合は、キャンセル処理と例外処理の役割を十分に理解しておいてください。たとえば、並列処理ツリーでタスクを取り消すと、子タスクも実行されなくなります。そのため、アプリケーションで重要となる操作 (リソースの解放など) が子タスクのいずれかで実行されるような場合に問題となります。また、子タスクが例外をスローすると、その例外がオブジェクトのデストラクターを介して反映され、アプリケーションで未定義の動作が発生する可能性があります。これらの点を示す例については、「並列パターン ライブラリに関するベスト プラクティス」の「Understand how Cancellation and Exception Handling Affect Object Destruction」を参照してください。PPL でのキャンセル モデルと例外処理モデルの詳細については、「PPL における取り消し処理」および「同時実行ランタイムでの例外処理」を参照してください。

[上]

関連トピック

タイトル

説明

方法: 並列呼び出しを使用して並列並べ替えルーチンを記述する

parallel_invoke アルゴリズムを使用して、バイトニック ソート アルゴリズムのパフォーマンスを向上させる方法について説明します。

方法: 並列呼び出しを使用して並列操作を実行する

parallel_invoke アルゴリズムを使用して、共有データ ソースに対して複数の操作を実行するプログラムのパフォーマンスを向上させる方法について説明します。

方法: 遅延後に完了するタスクを作成する

taskcancellation_token_sourcecancellation_tokentask_completion_event のクラスを遅延の後で、タスクの完了を作成する方法を示します。

チュートリアル: フューチャの実装

同時実行ランタイムの既存の機能を組み合わせて、より効果的に使用する方法を示します。

並列パターン ライブラリ (PPL)

同時実行アプリケーションの開発に不可欠なプログラミング モデルを提供する PPL について説明します。

参照

task クラス (同時実行ランタイム)

task_completion_event クラス

when_all 関数

when_any 関数

task_group クラス

parallel_invoke 関数

structured_task_group クラス