Параллелизм задач (среда выполнения с параллелизмом)

В этом документе описана роль задач и групп задач в среде выполнения с параллелизмом. Группы задач используются, если нужно параллельно выполнять два или более независимых рабочих элемента. Предположим, например, что имеется рекурсивный алгоритм, разделяющий оставшуюся работу на два раздела. Группы задач можно использовать, чтобы выполнять эти разделы параллельно. С другой стороны, можно использовать параллельные алгоритмы, например Concurrency::parallel_for, если нужно параллельно применить одну процедуру ко всем элементам коллекции. Дополнительные сведения об алгоритмах параллельной обработки см. в разделе Параллельные алгоритмы.

Задачи и группы задач

Задача — это единица работы, выполняющая конкретные действия. Задачи чаще всего могут выполняться параллельно с другими задачами и их можно разложить на дополнительные, более мелкие задачи. Группа задач упорядочивает коллекцию задач. Группы задач помещают задачи в очередь переноса нагрузки. Планировщик удаляет задачи из этой очереди и выполняет их с использованием доступных вычислительных ресурсов. После добавления задач в группу можно ожидать выполнения всех задач или отменять не начатые задачи.

PPL использует классы Concurrency::task_group и Concurrency::structured_task_group, чтобы представлять группы задач, и класс Concurrency::task_handle — чтобы представлять задачи. Класс task_handle содержит код, выполняющий работу. Этот код имеет форму лямбда-функции, указателя функции или объекта функции и часто называется рабочей функцией. Непосредственно с объектами 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 и дождаться завершения задач в другой функции или потоке.

Группы задач поддерживают принцип отмены. Отмена позволяет сообщить всем активным задачам, что необходимо отменить общую операцию. Отмена также предотвращает запуск еще не начатых задач. Дополнительные сведения об отмене см. в разделе Отмена в библиотеке параллельных шаблонов.

Среда выполнения также предоставляет модель обработки исключений, которая позволяет создать исключение из задачи и обработать его, если необходимо завершить связанную группу задач. Дополнительные сведения о модели обработки исключений см. в разделе Обработка исключений в среде выполнения с параллелизмом.

Сравнение task_group и structured_task_group

Рекомендуется использовать task_group или parallel_invoke вместо класса structured_task_group, но в некоторых случаях может быть нужно использовать класс 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. Дочерняя задача может вызывать эти методы для отмены родительской группы задач или проверки отмены в любой момент времени.

В объекте task_group можно выполнять дополнительные задачи после вызова метода Concurrency::task_group::wait или Concurrency::task_group::run_and_wait. И наоборот, нельзя выполнять дополнительные задачи в объекте structured_task_group после вызова метода Concurrency::structured_task_group::wait или Concurrency:: structured_task_group::run_and_wait.

Так как класс 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_group и structured_task_group поддерживают отмену. Дополнительные сведения об отмене см. в разделе Отмена в библиотеке параллельных шаблонов.

Пример

В следующем общем примере показано, как работать с группами задач. В этом примере алгоритм 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 для выполнения параллельных операций. Полный пример использования класса Walkthrough: Implementing Futures для реализации асинхронных фьючерсов см. в разделе Пошаговое руководство. Реализация фьючерсов.

Надежное программирование

Обязательно разберитесь в ролях отмены и обработки исключений при использовании групп задач и параллельных алгоритмов. Например, в дереве параллельной работы отмененная задача не позволяет выполняться дочерним задачам. Это может привести к проблемам, если одна из дочерних задач выполняет операцию, важную для приложения, например высвобождает ресурс. Кроме того, если дочерняя задача создает исключение, оно может распространиться через деструктор объекта и вызвать неопределенное поведение приложения. Пример, иллюстрирующий эти случаи, см. в разделе Знайте, как отмена и обработка исключений влияет на деструкцию объектов документа "Рекомендации по использованию библиотеки параллельных шаблонов". Дополнительные сведения о моделях отмены и обработки исключений в PPL см. в разделах Отмена в библиотеке параллельных шаблонов и Обработка исключений в среде выполнения с параллелизмом.

Связанные разделы

Ссылки

Класс task_group

Функция parallel_invoke

Класс structured_task_group

Журнал изменений

Дата

Журнал

Причина

Март 2011

Добавлены сведения о ролях отмены и обработки исключений при использовании групп задач и параллельных алгоритмов.

Улучшение информации.

Июль 2010

Содержимое реорганизовано.

Улучшение информации.

Май 2010

Рекомендации расширены.

Улучшение информации.