Отмена в библиотеке параллельных шаблонов

В этом разделе рассматривается роль операции отмены в библиотеке параллельных шаблонов (PPL), а также объясняется, как отменить параллельную работу и как определить, что группа задач отменена.

Подразделы

  • Деревья параллельной работы

  • Отмена параллельных задач

  • Отмена параллельных алгоритмов

  • Когда не следует использовать отмену

Деревья параллельной работы

В PPL для управления конкретными задачами и вычислениями используются группы задач. Можно вкладывать группы задач одна в другую, чтобы создавать деревья параллельной работы. На следующем рисунке показано дерево параллельной работы. На этом рисунке tg1 и tg2 представляют группы задач; а t1, t2, t3, t4и t5 — задачи.

Дерево параллельной работы

В следующем примере показан код, необходимый для создания дерева, изображенного на рисунке. В этом примере tg1 и tg2 являются объектами Concurrency::structured_task_group; а t1, t2, t3, t4 и t5 — объектами Concurrency::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::cancel или Concurrency::structured_task_group::cancel. Во-вторых, можно создать исключение в теле рабочей функции задачи.

Метод 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 на рисунке дерева параллельной работы, это повлияет на все задачи в этом дереве (t1, t2, t3, t4 и t5). Если отменить вложенную группу задач, tg2, это повлияет только на задачи t4 и t5.

При вызове метода 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. Это влияет на все задачи в дереве (t1, t2, t3, t4 и 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 среда выполнения удаляет любые задачи, выполнение которых еще не было начато, и не принимает новые задачи.

Третий пример напоминает второй за исключением того, что задача 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. Как и во втором примере, из-за этого обе группы задач в дереве (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 создают исключение, когда создает исключение дочерняя задача, пользователь не получает от них возвращаемого значения.

Как определить, что работа отменена

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

Если дочерняя задача выполняет времязатратную операцию и не совершает вызов в среде выполнения, она должна периодически проверять отмену и своевременно выполнять выход. В следующем примере показан один из способов определить отмену работы. Задача 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();

В этом примере проверка отмены выполняется каждую сотую итерацию цикла задач. Частота проверки отмены зависит от объема работы, выполняемой задачей, и тем, насколько быстро задачи должны реагировать на отмену.

Если отсутствует доступ к объекту родительской группы задач, следует вызвать функцию 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

Добавлен еще один случай в раздел "Когда не следует использовать отмену".

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