任务并行(并发运行时)

更新:2011 年 3 月

本文档介绍并发运行时中任务和任务组的角色。 当您具有两个或更多想要同时运行的独立工作项时,可使用任务组。 例如,假设您使用递归算法将剩余工时分成两部分。 您可以使用任务组同时运行这些分区。 相反,当您想要对集合中的每个元素并行应用相同的例程时,可使用并行算法,例如 Concurrency::parallel_for。 有关并行算法的更多信息,请参见并行算法

任务和任务组

“任务”是执行特定作业的工作单元。 任务通常可以与其他任务并行运行,并且可以分解成更多且更细化的任务。 “任务组”可组织任务的集合。 任务组会将任务推入工作窃取队列。 计划程序从该队列中移除任务,并用可用的计算资源执行任务。 在将任务添加到任务组之后,您可以等待所有任务完成或取消尚未开始的任务。

PPL 使用 Concurrency::task_groupConcurrency::structured_task_group 类表示任务组,使用 Concurrency::task_handle 类表示任务。 task_handle 类封装执行工作的代码。 该代码以 lambda 函数、函数指针或函数对象的形式出现,通常被称为工作函数。 通常不需要直接使用 task_handle 对象。 而是可以将工作函数传递给任务组,然后任务组会创建和管理 task_handle 对象。

PPL 将任务组分为以下两类:非结构化的任务组和结构化的任务组。 PPL 使用 task_group 类来表示非结构化任务组,使用 structured_task_group 类来表示结构化任务组。

重要说明重要事项

PPL 还定义 Concurrency::parallel_invoke 算法,后者使用 structured_task_group 类并行执行一组任务。 由于 parallel_invoke 算法的语法比较简单,因此建议您尽可能使用该算法而非 structured_task_group 类。 主题并行算法更加详细地介绍了parallel_invoke

当您需要同时执行多个独立任务并且必须等待所有任务完成才能继续时,可使用 parallel_invoke。 当您需要同时执行多个独立任务但希望稍后再等待任务完成时,可使用 task_group。 例如,您可以将任务添加到 task_group 对象中,然后在另一个函数中或从另一个线程等待任务完成。

任务组支持取消这一概念。 取消允许您向要取消整体操作的所有活动任务发送信号。 取消还阻止尚未开始的任务开始执行。 有关取消操作的更多信息,请参见 PPL 中的取消操作

运行时还提供异常处理模型,以允许您从任务引发异常并在等待关联任务组完成时处理该异常。 有关该异常处理模型的更多信息,请参见并发运行时中的异常处理

task_group 与 structured_task_group 的比较

尽管建议您使用 task_groupparallel_invoke 替代 structured_task_group 类,但是在某些情况下您可能还是希望使用 structured_task_group,例如在编写执行可变数量任务或需要取消支持的算法的情况下。 本节说明 task_groupstructured_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::waitConcurrency::task_group::run_and_wait 方法之后,在 task_group 对象上运行其他任务。 相反,在调用 Concurrency::structured_task_group::waitConcurrency:: 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_groupstructured_task_group 都支持取消操作。 有关取消操作的更多信息,请参见 PPL 中的取消操作

示例

下面的基本示例演示如何使用任务组。 本示例使用 parallel_invoke 算法并发执行两个任务。 每个任务都将子任务添加到 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 算法的完整示例,请参见如何:使用 parallel_invoke 来编写并行排序运行时如何:使用 parallel_invoke 来执行并行操作。 有关使用 task_group 类实现异步功能的完整示例,请参见演练:实现 Future

可靠编程

请确保在使用任务组和并行算法时您已了解取消和异常处理的作用。 例如,在并行工作树中,取消的任务阻止子任务运行。 如果某个子任务执行的操作对于应用程序很重要(例如释放资源),则这可能会引发问题。 另外,如果子任务引发异常,该异常可能在对象析构函数中传播并导致您的应用程序出现未定义的行为。 有关说明这些问题的示例,请参见“并行模式库中的最佳做法”文档中的了解取消和异常处理如何影响对象销毁一节。 有关 PPL 中取消和异常处理模型的更多信息,请参见PPL 中的取消操作并发运行时中的异常处理

相关主题

参考

task_group 类

parallel_invoke 函数

structured_task_group 类

修订记录

日期

修订记录

原因

2011 年 3 月

添加了有关使用任务组和并行算法时取消和异常处理角色的信息。

信息补充。

2010 年 7 月

重新组织了内容。

信息补充。

2010 年 5 月

扩充了指导内容。

信息补充。