PPL 中的取消操作

更新:2011 年 3 月

本主题说明并行模式库 (PPL) 中取消操作的角色、如何取消并行工作以及如何确定取消任务组的时间。

各节内容

  • 并行工作树

  • 取消并行任务

  • 取消并行算法

  • 何时不使用取消

并行工作树

PPL 使用任务组来管理细化的任务和计算。 可以嵌套任务组,以形成并行工作树。 下图演示了并行工作树。 在该图中,tg1tg2 表示任务组;t1t2t3t4t5 表示任务。

并行工作树

以下示例演示了创建该图中的树所需的代码。 在此示例中,tg1tg2Concurrency::structured_task_group 对象;t1t2t3t4t5Concurrency::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();   
}

[转到页首]

取消并行任务

可以通过两种方式取消并行工作。 其中一种方式是调用 Concurrency::task_group::cancelConcurrency::structured_task_group::cancel 方法。 另一种方式是在任务工作函数体中引发异常。

在取消并行工作树时,cancel 方法的效率比异常处理高。 cancel 方法取消任务组,并以自上而下的方式取消所有子任务组。 相反,异常处理以自下而上的方式工作,并且必须在异常向上传播时单独取消每个子任务组。

以下各节说明如何使用 cancel 方法和异常处理来取消并行工作。 有关取消并行任务的更多示例,请参见如何:使用取消中断 Parallel 循环如何:使用异常处理中断 Parallel 循环

使用取消方法来取消并行工作

Concurrency::task_group::cancelConcurrency::structured_task_group::cancel 方法可将任务组设置为已取消状态。

提示

运行时使用异常处理实现取消操作。 不要在代码中捕捉或处理这些异常。 此外,还建议您在任务的函数体中编写异常安全的代码。 例如,可以使用“获取资源即初始化”(RAII) 模式,以确保在任务体中引发异常时正确处理资源。 有关使用 RAII 模式清理可取消任务中的资源的完整示例,请参阅演练:从用户界面线程中移除工作

在您调用 cancel 后,任务组不会启动将来的任务。 cancel 方法可以由多个子任务调用。 已取消状态会导致 Concurrency::task_group::waitConcurrency::structured_task_group::wait 方法返回 Concurrency::canceled

cancel 方法只影响子任务。 例如,如果取消并行工作树插图中的任务组 tg1,则该树中的所有任务(t1t2t3t4t5)都将受到影响。 如果取消嵌套的任务组 tg2,则只有任务 t4t5 会受到影响。

当您调用 cancel 方法时,将同时取消所有子任务组。 但是,取消操作并不影响并行工作树中任务组的任何父级。 下面的示例通过在并行工作树插图中生成来演示这一点。

这些示例中的第一个示例为任务 t4(该任务是任务组 tg2 的子级)创建一个工作函数。 该工作函数在循环中调用函数 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。 这会影响树中的所有任务(t1t2t3t4t5)。

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::cancelConcurrency::structured_task_group::is_canceling 方法。 子任务可以调用这些方法来取消父任务和检查取消。

使用异常来取消并行工作

并发运行时中的异常处理主题解释并发运行时如何使用异常来传递错误。 但是,并非所有异常都表示错误。 例如,搜索算法可能在找到结果时取消其关联的任务组。 但是,如上所述,在取消并行工作时,异常处理的效率比使用 cancel 方法低。

当您在传递给任务组的工作函数体中引发异常时,运行时存储该异常,并将该异常封送到等待任务组完成的上下文。 像 cancel 方法一样,运行时将放弃任何尚未启动的任务,并且不接受新任务。

第三个示例与第二个示例类似,只不过任务 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;
}

第四个示例使用异常处理来取消整个工作树。 此示例在任务组 tg1 等待其子任务完成时(而不是任务组 tg2 等待其子任务完成时)捕获异常。 与第二个示例类似,这会导致树中的两个任务组 tg1tg2 都进入已取消状态。

// 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::waitstructured_task_group::wait 方法在子任务引发异常时引发,所以您没有从它们收到返回值。

确定取消工作的时间

取消是协作性的。 因此,它不会立即发生。 如果任务组已取消,从每个子任务到运行时的调用可以触发一个中断点,这将导致运行时引发和捕获内部异常类型以取消活动任务。 并发运行时不定义具体中断点;它们可以在对运行时的任何调用中出现。 运行时必须处理它引发的异常,才能执行取消。 因此,请不要处理任务体中的未知异常。

如果子任务执行耗时的操作,并且不调用运行时,它必须定期检查取消并及时退出。 下面的示例演示一种确定取消工作的时间的方法。 任务 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 月

在“何时不使用取消”一节中添加了另一种情况。

信息补充。