PPL における取り消し処理

更新 : 2011 年 3 月

ここでは、並列パターン ライブラリ (PPL: Parallel Patterns Library) での取り消し処理の役割、並列処理を取り消す方法、およびタスク グループの取り消しを判定する方法について説明します。

セクション

  • 並列処理ツリー

  • 並列タスクの取り消し

  • 並列アルゴリズムの取り消し

  • 取り消し処理が適さないケース

並列処理ツリー

PPL は、細かく分類されたタスクおよび計算を管理するためにタスク グループを使用します。 タスク グループを入れ子にすることで、並列処理のツリーを形成できます。 並列処理ツリーの図を次に示します。 この図では、tg1tg2 がタスク グループを表し、t1t2t3t4、および t5 がタスクを表しています。

並列処理ツリー

前の図のツリーを作成するために必要なコード例を次に示します。 この例では、tg1tg2Concurrency::structured_task_group オブジェクトで、t1t2t3t4、および t5Concurrency::task_handle オブジェクトです。

// task-tree.cpp
// compile with: /c /EHsc
#include <ppl.h>
#include <sstream>
#include <iostream>
#include <sstream>

using namespace Concurrency;
using namespace std;

void create_task_tree()
{   
   // Create a task group that serves as the root of the tree.
   structured_task_group tg1;

   // Create a task that contains a nested task group.
   auto t1 = make_task([&] {
      structured_task_group tg2;

      // Create a child task.
      auto t4 = make_task([&] {
         // TODO: Perform work here.
      });

      // Create a child task.
      auto t5 = make_task([&] {
         // TODO: Perform work here.
      });

      // Run the child tasks and wait for them to finish.
      tg2.run(t4);
      tg2.run(t5);
      tg2.wait();
   });

   // Create a child task.
   auto t2 = make_task([&] {
      // TODO: Perform work here.
   });

   // Create a child task.
   auto t3 = make_task([&] {
      // TODO: Perform work here.
   });

   // Run the child tasks and wait for them to finish.
   tg1.run(t1);
   tg1.run(t2);
   tg1.run(t3);
   tg1.wait();   
}

[ページのトップへ]

並列タスクの取り消し

並列処理を取り消すには 2 とおりの方法があります。 1 つは、Concurrency::task_group::cancel メソッドまたは Concurrency::structured_task_group::cancel メソッドを呼び出す方法です。 もう 1 つは、タスクの処理関数の本体で例外をスローする方法です。

並列処理ツリーを取り消す場合は、cancel メソッドを使用する方が、例外処理を行うよりも効率的です。 cancel メソッドは、タスク グループと子タスク グループを上位から順に取り消します。 反対に、例外処理では下位から順に処理されるため、例外が上位へ移動するたびに、子タスク グループを個別に取り消す必要があります。

以降のセクションでは、cancel メソッドおよび例外処理を使用して並列処理を取り消す方法を示します。 並列タスクを取り消すその他の例については、「方法: キャンセル処理を使用して並列ループを中断する」および「方法: 例外処理を使用して並列ループを中断する」を参照してください。

cancel メソッドを使用した並列処理の取り消し

Concurrency::task_group::cancel メソッドおよび Concurrency::structured_task_group::cancel メソッドは、タスク グループを取り消された状態に設定します。

注意

ランタイムは例外処理を使用して取り消し処理を実装します。 これらの例外をコードでキャッチまたは処理しないでください。 さらに、タスクの関数本体では例外セーフなコードを作成することをお勧めします。 たとえば、Resource Acquisition Is Initialization (RAII) パターンを使用すると、例外がタスクの本体でスローされたときに、リソースを正しく処理することができます。 取り消し可能なタスクで RAII パターンを使用してリソースをクリーンアップする完全な例については、「チュートリアル: ユーザー インターフェイス スレッドからの処理の除去」を参照してください。

cancel を呼び出すと、それ以降はタスク グループでタスクが開始されなくなります。 cancel メソッドは、複数の子タスクから呼び出すことができます。 タスクを取り消すと、Concurrency::task_group::wait メソッドおよび Concurrency::structured_task_group::wait メソッドで Concurrency::canceled が返されるようになります。

cancel メソッドは、子タスクにのみ影響を及ぼします。 たとえば、並列処理ツリーの図のタスク グループ tg1 を取り消すと、ツリー内のすべてのタスク (t1t2t3t4、および t5) が取り消されます。 入れ子になったタスク グループ tg2 を取り消すと、タスク t4 および t5 のみ影響を受けます。

cancel メソッドを呼び出すと、子タスク グループもすべて取り消されます。 しかし、取り消し処理は、並列処理ツリーにおけるタスク グループの親には影響しません。 以降の例では、並列処理ツリーの図に基づいてこのことを示します。

最初の例では、タスク グループ tg2 の子であるタスク t4 の処理関数を作成します。 この処理関数は、関数 work をループ内で呼び出します。 work の呼び出しが失敗すると、タスクは親タスク グループを取り消します。 これにより、タスク グループ tg2 が取り消された状態になりますが、タスク グループ tg1 は取り消されません。

auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel the parent task
      // and break from the loop.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }         
});

次の例は、最初の例と似ていますが、タスクがタスク グループ tg1 を取り消す点が異なっています。 これにより、ツリー内のすべてのタスク (t1t2t3t4、および t5) が影響を受けます。

auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel all tasks in the tree.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg1.cancel();
         break;
      }
   }   
});

structured_task_group クラスはスレッドセーフではありません。 したがって、子タスクが親 structured_task_group オブジェクトのメソッドを呼び出すと、未定義の動作が実行されます。 ただし、structured_task_group::cancel メソッドと Concurrency::structured_task_group::is_canceling メソッドには、この規則が適用されません。 子タスクがこれらのメソッドを呼び出すことで、親タスク グループを取り消すことや、取り消し状態をチェックすることができます。

例外を使用した並列処理の取り消し

同時実行ランタイムでの例外処理」では、同時実行ランタイムが例外を使用してエラーを通知するしくみについて説明しています。 しかし、すべての例外がエラーの発生を示す訳ではありません。 たとえば、検索アルゴリズムでは、結果の検出時に関連付けられたタスク グループが取り消される場合があります。 しかし、前述したように、並列処理の取り消しを行う場合、例外処理は cancel メソッドを使用するよりも効率の点で劣ります。

タスク グループに渡す処理関数の本体で例外をスローすると、ランタイムはその例外を保存して、タスク グループの終了を待機するコンテキストにその例外をマーシャリングします。 cancel メソッドの場合と同様に、ランタイムはまだ開始されていないタスクを破棄し、新しいタスクを受け付けません。

次の 3 つ目の例は、2 つ目の例と似ていますが、タスク t4 が例外をスローすることでタスク グループ tg2 を取り消している点が異なります。 この例では、try-catch ブロックを使用して、タスク グループ tg2 が子タスクの終了を待機しているときに取り消し状態をチェックします。 最初の例と同様に、これによってタスク グループ tg2 が取り消された状態になりますが、タスク グループ tg1 は取り消されません。

structured_task_group tg2;

// Create a child task.      
auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, throw an exception to 
      // cancel the parent task.
      bool succeeded = work(i);
      if (!succeeded)
      {
         throw exception("The task failed");
      }
   }         
});

// Create a child task.
auto t5 = make_task([&] {
   // TODO: Perform work here.
});

// Run the child tasks.
tg2.run(t4);
tg2.run(t5);

// Wait for the tasks to finish. The runtime marshals any exception
// that occurs to the call to wait.
try
{
   tg2.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

次の 4 つ目の例では、例外処理を使用して処理ツリー全体を取り消します。 この例では、タスク グループ tg2 が子タスクの終了を待機しているときではなく、タスク グループ tg1 が子タスクの終了を待機しているときに、例外をキャッチします。 2 番目の例と同様に、これによってツリー内の 2 つのタスク グループ tg1 および tg2 の両方が取り消された状態になります。

// Run the child tasks.
tg1.run(t1);
tg1.run(t2);
tg1.run(t3);   

// Wait for the tasks to finish. The runtime marshals any exception
// that occurs to the call to wait.
try
{
   tg1.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

task_group::wait メソッドと structured_task_group::wait メソッドは、子タスクが例外をスローしたときに実行されるため、これらのメソッドから戻り値を受け取ることはありません。

処理の取り消し状態の判定

取り消し処理は他の処理と連携して行われます。 このため、すぐに実行される訳ではありません。 タスク グループが取り消されると、各子タスクからのランタイムの呼び出しによって割り込みポイントが発生する場合があります。このような場合、ランタイムは内部的な例外をスローおよびキャッチして、実行中のタスクを取り消します。 同時実行ランタイムでは、特定の割り込みポイントが定義されていません。割り込みポイントは、ランタイムに対する任意の呼び出しで発生します。 ランタイムは、取り消し処理を実行するために、自身がスローした例外を処理する必要があります。 このため、タスクの本体で不明な例外を処理しないでください。

子タスクが時間のかかる処理を実行しており、ランタイムの呼び出しを行わない場合、子タスクは定期的に取り消し状態をチェックして、適切なタイミングで終了する必要があります。 処理の取り消し状態を判定する方法の 1 つを次の例に示します。 タスク t4 は、エラーの発生時に親タスク グループを取り消します。 タスク t5 は、structured_task_group::is_canceling メソッドを定期的に呼び出して、取り消し状態をチェックします。 親タスク グループが取り消されると、タスク t5 はメッセージを表示して終了します。

structured_task_group tg2;

// Create a child task.
auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel the parent task
      // and break from the loop.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }
});

// Create a child task.
auto t5 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // To reduce overhead, occasionally check for 
      // cancelation.
      if ((i%100) == 0)
      {
         if (tg2.is_canceling())
         {
            wcout << L"The task was canceled." << endl;
            break;
         }
      }

      // TODO: Perform work here.
   }
});

// Run the child tasks and wait for them to finish.
tg2.run(t4);
tg2.run(t5);
tg2.wait();

この例では、タスク ループが 100 回繰り返されるたびに取り消し状態がチェックされます。 取り消し状態をチェックする頻度は、タスクで実行される処理の量と、取り消し処理にタスクがどの程度すばやく応答する必要があるのかによって決まります。

親タスク グループ オブジェクトにアクセスできない場合は、Concurrency::is_current_task_group_canceling 関数を呼び出して、親タスク グループが取り消されたかどうかを確認します。

[ページのトップへ]

並列アルゴリズムの取り消し

PPL の並列アルゴリズム (Concurrency::parallel_for など) は、タスク グループを基準として構築されています。 このため、これまで説明した方法のうちの多くを使用して、並列アルゴリズムを取り消すことができます。

以降の例では、並列アルゴリズムを取り消すためのいくつかの方法を示します。

Concurrency::structured_task_group::run_and_wait メソッドを使用して parallel_for アルゴリズムを呼び出す例を次に示します。 structured_task_group::run_and_wait メソッドは、指定されたタスクの終了を待機します。 structured_task_group オブジェクトによって、処理関数でタスクを取り消すことができるようになります。

// To enable cancelation, call parallel_for in a task group.
structured_task_group tg;

task_group_status status = tg.run_and_wait([&] {
   parallel_for(0, 100, [&](int i) {
      // Cancel the task when i is 50.
      if (i == 50)
      {
         tg.cancel();
      }
      else
      {
         // TODO: Perform work here.
      }
   });
});

// Print the task group status.
wcout << L"The task group status is: ";
switch (status)
{
case not_complete:
   wcout << L"not complete." << endl;
   break;
case completed:
   wcout << L"completed." << endl;
   break;
case canceled:
   wcout << L"canceled." << endl;
   break;
default:
   wcout << L"unknown." << endl;
   break;
}

この例を実行すると、次の出力が生成されます。

The task group status is: canceled.

例外処理を使用して parallel_for ループを取り消す例を次に示します。 ランタイムは、例外を呼び出し元のコンテキストにマーシャリングします。

try
{
   parallel_for(0, 100, [&](int i) {
      // Throw an exception to cancel the task when i is 50.
      if (i == 50)
      {
         throw i;
      }
      else
      {
         // TODO: Perform work here.
      }
   });
}
catch (int n)
{
   wcout << L"Caught " << n << endl;
}

この例を実行すると、次の出力が生成されます。

Caught 50

ブール型のフラグを使用して parallel_for ループに取り消し処理を組み込む例を次に示します。 この例では、cancel メソッドや例外処理を使用して一連のタスク全体を取り消している訳ではないため、すべてのタスクが実行されます。 したがって、この方法では、取り消しメソッドを使用するよりも多くの計算オーバーヘッドが発生する場合があります。

// Create a Boolean flag to coordinate cancelation.
bool canceled = false;

parallel_for(0, 100, [&](int i) {
   // For illustration, set the flag to cancel the task when i is 50.
   if (i == 50)
   {
      canceled = true;
   }

   // Perform work if the task is not canceled.
   if (!canceled)
   {
      // TODO: Perform work here.
   }
});

どの取り消し方法にも、他の方法にはない利点があります。 特定のニーズに合った方法を採用してください。

[ページのトップへ]

取り消し処理が適さないケース

取り消し処理は、関連するタスク グループの各メンバーが適時に終了できる場合には適しています。 しかし、取り消し処理がアプリケーションに適さないケースもあります。 たとえば、タスクの取り消しは他の処理と連携して行われるため、個別のタスクがブロックされている場合は、一連のタスク全体を取り消すことができなくなります。 また、あるタスクがまだ開始されていないが、他の実行中のタスクのブロックを解除する場合、タスク グループが取り消されると、そのタスクは開始されません。 これにより、アプリケーションでデッドロックが発生する場合があります。 また、取り消し対象のタスクの子タスクで重要な操作 (リソースの解放など) が実行されるような状況でも、取り消し処理の使用はふさわしくありません。 親タスクを取り消すと一連のタスクがすべて取り消されるため、その操作は実行されなくなります。 この点を示す例については、「並列パターン ライブラリに関するベスト プラクティス」の「取り消し処理および例外処理がオブジェクトの破棄に及ぼす影響について」を参照してください。

[ページのトップへ]

関連トピック

参照

task_group クラス

structured_task_group クラス

parallel_for 関数

履歴の変更

日付

履歴

理由

2011 年 3 月

「取り消し処理が適さないケース」に別のケースを追加。

情報の拡充