Поделиться через


Рекомендации по работе с библиотекой параллельных шаблонов

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

Дополнительные сведения о PPL См. в разделе Библиотека параллельных шаблонов.

Подразделы

Этот документ содержит следующие разделы.

  • Не выполняйте небольшие основные части циклов параллельно

  • Реализуйте параллелизм на самом высоком уровне

  • Используйте parallel_invoke, чтобы разрешать проблемы типа "разделяй и властвуй"

  • Используйте отмену или обработку исключений, чтобы выйти из параллельного цикла

  • Знайте, как отмена и обработка исключений влияет на деструкцию объектов

  • Не выполняйте многократную блокировку в параллельном цикле

  • Не выполняйте операции блокировки при отмене параллельных задач

  • Не выполняйте запись в общие данные в параллельном цикле

  • По возможности избегайте ложного совместного использования

  • Следите, чтобы переменные были допустимы в течение всего времени существования задачи

Не выполняйте небольшие основные части циклов параллельно

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

// small-loops.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

int wmain()
{
   // Create three arrays that each have the same size.
   const size_t size = 100000;
   int a[size], b[size], c[size];

   // Initialize the arrays a and b.
   for (size_t i = 0; i < size; ++i)
   {
      a[i] = i;
      b[i] = i * 2;
   }

   // Add each pair of elements in arrays a and b in parallel 
   // and store the result in array c.
   parallel_for<size_t>(0, size, [&a,&b,&c](size_t i) {
      c[i] = a[i] + b[i];
   });

   // TODO: Do something with array c.
}

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

[в начало]

Реализуйте параллелизм на самом высоком уровне

Если используется параллелизация кода только на низком уровне, может возникнуть вилочное соединение, которое не масштабируется по мере увеличения числа процессоров. Вилочное соединение — это структура, в которой задача разделяет работу на более маленькие параллельные подзадачи и ожидает их выполнения. Каждая подзадача может рекурсивно делиться на более мелкие подзадачи.

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

// Calls the provided function for each pixel in a Bitmap object.
void ProcessImage(Bitmap* bmp, const function<void (DWORD&)>& f)
{
   int width = bmp->GetWidth();
   int height = bmp->GetHeight();

   // Lock the bitmap.
   BitmapData bitmapData;
   Rect rect(0, 0, bmp->GetWidth(), bmp->GetHeight());
   bmp->LockBits(&rect, ImageLockModeWrite, PixelFormat32bppRGB, &bitmapData);

   // Get a pointer to the bitmap data.
   DWORD* image_bits = (DWORD*)bitmapData.Scan0;

   // Call the function for each pixel in the image.
   for (int y = 0; y < height; ++y)
   {      
      for (int x = 0; x < width; ++x)
      {
         // Get the current pixel value.
         DWORD* curr_pixel = image_bits + (y * width) + x;

         // Call the function.
         f(*curr_pixel);
      }
   }

   // Unlock the bitmap.
   bmp->UnlockBits(&bitmapData);
}

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

// Calls the provided function for each pixel in a Bitmap object.
void ProcessImage(Bitmap* bmp, const function<void (DWORD&)>& f)
{
   int width = bmp->GetWidth();
   int height = bmp->GetHeight();

   // Lock the bitmap.
   BitmapData bitmapData;
   Rect rect(0, 0, bmp->GetWidth(), bmp->GetHeight());
   bmp->LockBits(&rect, ImageLockModeWrite, PixelFormat32bppRGB, &bitmapData);

   // Get a pointer to the bitmap data.
   DWORD* image_bits = (DWORD*)bitmapData.Scan0;

   // Call the function for each pixel in the image.
   parallel_for (0, height, [&, width](int y)
   {      
      for (int x = 0; x < width; ++x)
      {
         // Get the current pixel value.
         DWORD* curr_pixel = image_bits + (y * width) + x;

         // Call the function.
         f(*curr_pixel);
      }
   });

   // Unlock the bitmap.
   bmp->UnlockBits(&bitmapData);
}

В следующем примере показано вилочное соединение: циклический вызов функции ProcessImage. Вызовы к ProcessImage не выполняют возврат, пока не будут выполнены все подзадачи.

// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
   for_each(bitmaps.begin(), bitmaps.end(), [&f](Bitmap* bmp) {
      ProcessImage(bmp, f);
   });
}

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

Чтобы уменьшить дополнительную нагрузку планирования в этом примере, можно распараллелить внешние циклы раньше, чем внутренние, или использовать другие параллельные структуры, например конвейеризацию. Следующий пример изменяет функцию ProcessImages так, чтобы использовать алгоритм Concurrency::parallel_for_each для распараллеливания внешнего цикла.

// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
   parallel_for_each(bitmaps.begin(), bitmaps.end(), [&f](Bitmap* bmp) {
      ProcessImage(bmp, f);
   });
}

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

[в начало]

Используйте parallel_invoke, чтобы разрешать проблемы типа "разделяй и властвуй"

Проблема разделяй и властвуй — это разновидность структуры вилочного соединения, использующая рекурсию для разбиения задачи на подзадачи. Помимо классов Concurrency::task_group и Concurrency::structured_task_group можно использовать алгоритм Concurrency::parallel_invoke, чтобы разрешать проблемы типа "разделяй и властвуй". У алгоритма parallel_invoke более сжатый синтаксис, чем у объектов групп задач, поэтому он полезен для фиксированного количества параллельных задач.

В следующем примере показано использование алгоритма parallel_invoke для реализации алгоритма битонной сортировки.

// Sorts the given sequence in the specified order.
template <class T>
void parallel_bitonic_sort(T* items, int lo, int n, bool dir)
{   
   if (n > 1)
   {
      // Divide the array into two partitions and then sort 
      // the partitions in different directions.
      int m = n / 2;

      parallel_invoke(
         [&] { parallel_bitonic_sort(items, lo, m, INCREASING); },
         [&] { parallel_bitonic_sort(items, lo + m, m, DECREASING); }
      );

      // Merge the results.
      parallel_bitonic_merge(items, lo, n, dir);
   }
}

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

Полную версию этого примера см. в разделе Практическое руководство. Использование функции parallel_invoke для написания программы параллельной сортировки. Дополнительные сведения об алгоритме parallel_invoke см. в разделе Параллельные алгоритмы.

[в начало]

Используйте отмену или обработку исключений, чтобы выйти из параллельного цикла

PPL предоставляет два способа отменить параллельную работу, выполняемую группой задач или параллельным алгоритмом. Один способ — использовать механизм отмены классов Concurrency::task_group и Concurrency::structured_task_group. Во-вторых, можно создать исключение в теле рабочей функции задачи. Механизм отмены более эффективен, чем обработка исключений при отмене дерева параллельной работы. Дерево параллельной работы — это группа групп связанных задач, в которой некоторые группы задач содержат другие группы. Механизм отмены отменяет группу задач и ее дочерние группы задач сверху вниз. Напротив, обработка исключений осуществляется снизу вверх. При этом каждую дочернюю группу задач необходимо отменять отдельно, так как исключение распространяется вверх.

При работе непосредственно с объектом группы задач для отмены работы, принадлежащей этой группе задач, используется метод Concurrency::task_group::cancel или Concurrency::structured_task_group::cancel. Чтобы отменить параллельный алгоритм, например parallel_for, следует создать родительскую группу задач и отменить ее. Например, следующая функция, parallel_find_any, выполняет параллельный поиск значения в массиве.

// Returns the position in the provided array that contains the given value, 
// or -1 if the value is not in the array.
template<typename T>
int parallel_find_any(const T a[], size_t count, const T& what)
{
   // The position of the element in the array. 
   // The default value, -1, indicates that the element is not in the array.
   int position = -1;

   // Use parallel_for to search for the element. 
   // The task group enables a work function to cancel the overall 
   // operation when it finds the result.

   structured_task_group tasks;
   tasks.run_and_wait([&]
   {
      parallel_for(std::size_t(0), count, [&](int n) {
         if (a[n] == what)
         {
            // Set the return value and cancel the remaining tasks. 
            position = n;            
            tasks.cancel();
         }
      });
   });

   return position;
}

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

Хотя обработка исключений является менее эффективным способом отмены параллельной работы, чем механизм отмены, в некоторых случаях обработка исключений подходит хорошо. Например, следующий метод, for_all, рекурсивно выполняет рабочую функцию для всех узлов структуры tree. В этом примере член данных _children является объектом std::list, содержащим объекты tree.

// Performs the given work function on the data element of the tree and
// on each child.
template<class Function>
void tree::for_all(Function& action)
{
   // Perform the action on each child.
   parallel_for_each(_children.begin(), _children.end(), [&](tree& child) {
      child.for_all(action);
   });

   // Perform the action on this node.
   action(*this);
}

Вызывающая сторона метода tree::for_all может создать исключение, если не требует вызова рабочей функции для каждого элемента дерева. В следующем примере показана функция search_for_value, осуществляющая поиск значения в предоставленном объекте tree. Функция search_for_value использует рабочую функцию, которая создает исключение, если текущий элемент дерева соответствует предоставленному значению. Функция search_for_value использует блок try-catch, чтобы зафиксировать исключение и вывести результат на консоль.

// Searches for a value in the provided tree object.
template <typename T>
void search_for_value(tree<T>& t, int value)
{
   try
   {
      // Call the for_all method to search for a value. The work function
      // throws an exception when it finds the value.
      t.for_all([value](const tree<T>& node) {
         if (node.get_data() == value)
         {
            throw &node;
         }
      });
   }
   catch (const tree<T>* node)
   {
      // A matching node was found. Print a message to the console.
      wstringstream ss;
      ss << L"Found a node with value " << value << L'.' << endl;
      wcout << ss.str();
      return;
   }

   // A matching node was not found. Print a message to the console.
   wstringstream ss;
   ss << L"Did not find node with value " << value << L'.' << endl;
   wcout << ss.str();   
}

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

Более общие сведения о механизмах отмены и обработки исключений, предоставляемых PPL, см. в разделах Отмена в библиотеке параллельных шаблонов и Обработка исключений в среде выполнения с параллелизмом.

[в начало]

Знайте, как отмена и обработка исключений влияет на деструкцию объектов

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

В следующем примере класс Resource описывает ресурс, а класс Container описывает контейнер, содержащий ресурсы. В своем деструкторе класс Container параллельно вызывает метод cleanup для двух его членов Resource, а затем вызывает метод cleanup для третьего члена Resource.

// parallel-resource-destruction.h
#pragma once
#include <ppl.h>
#include <sstream>
#include <iostream>

// Represents a resource.
class Resource
{
public:
   Resource(const std::wstring& name)
      : _name(name)
   {
   }

   // Frees the resource.
   void cleanup()
   {
      // Print a message as a placeholder.
      std::wstringstream ss;
      ss << _name << L": Freeing..." << std::endl;
      std::wcout << ss.str();
   }
private:
   // The name of the resource.
   std::wstring _name;
};

// Represents a container that holds resources.
class Container
{
public:
   Container(const std::wstring& name)
      : _name(name)
      , _resource1(L"Resource 1")
      , _resource2(L"Resource 2")
      , _resource3(L"Resource 3")
   {
   }

   ~Container()
   {
      std::wstringstream ss;
      ss << _name << L": Freeing resources..." << std::endl;
      std::wcout << ss.str();

      // For illustration, assume that cleanup for _resource1
      // and _resource2 can happen concurrently, and that 
      // _resource3 must be freed after _resource1 and _resource2.

      Concurrency::parallel_invoke(
         [this]() { _resource1.cleanup(); },
         [this]() { _resource2.cleanup(); }
      );

      _resource3.cleanup();
   }

private:
   // The name of the container.
   std::wstring _name;

   // Resources.
   Resource _resource1;
   Resource _resource2;
   Resource _resource3;
};

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

// parallel-resource-destruction.cpp
// compile with: /EHsc
#include "parallel-resource-destruction.h"

using namespace Concurrency;
using namespace std;

static_assert(false, "This example illustrates a non-recommended practice.");

int main()
{  
   // Create a task_group that will run two tasks.
   task_group tasks;

   // Used to synchronize the tasks.
   event e1, e2;

   // Run two tasks. The first task creates a Container object. The second task
   // cancels the overall task group. To illustrate the scenario where a child 
   // task is not run because its parent task is cancelled, the event objects 
   // ensure that the Container object is created before the overall task is 
   // cancelled and that the Container object is destroyed after the overall 
   // task is cancelled.

   tasks.run([&tasks,&e1,&e2] {
      // Create a Container object.
      Container c(L"Container 1");

      // Allow the second task to continue.
      e2.set();

      // Wait for the task to be cancelled.
      e1.wait();
   });

   tasks.run([&tasks,&e1,&e2] {
      // Wait for the first task to create the Container object.
      e2.wait();

      // Cancel the overall task.
      tasks.cancel();      

      // Allow the first task to continue.
      e1.set();
   });

   // Wait for the tasks to complete.
   tasks.wait();

   wcout << L"Exiting program..." << endl;
}

После выполнения примера получается следующий результат.

Container 1: Freeing resources...
Exiting program...

Следующие проблемы в этом примере кода могут привести к неожиданному поведению.

  • Отмена родительской задачи приводит к отмене дочерней задачи — вызова Concurrency::parallel_invoke. Поэтому два эти ресурса не высвобождаются.

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

  • Исключение, созданное дочерней задачей, распространяется через деструктор Container. Исключение из деструктора приводит приложение в неопределенное состояние.

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

[в начало]

Не выполняйте многократную блокировку в параллельном цикле

Параллельный цикл, например Concurrency::parallel_for или Concurrency::parallel_for_each, в котором преобладают операции блокирования, может привести к созданию средой выполнения большого количества потоков за короткое время.

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

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

Рассмотрим следующий пример, вызывающий функцию Concurrency::send в каждой итерации цикла parallel_for. Так как функция send выполняет совместную блокировку, среда выполнения создает новый поток для дополнительной работы при каждом вызове send.

// repeated-blocking.cpp
// compile with: /EHsc
#include <ppl.h>
#include <agents.h>

using namespace Concurrency;

static_assert(false, "This example illustrates a non-recommended practice.");

int main()
{
   // Create a message buffer.
   overwrite_buffer<int> buffer;

   // Repeatedly send data to the buffer in a parallel loop.
   parallel_for(0, 1000, [&buffer](int i) {

      // The send function blocks cooperatively. 
      // We discourage the use of repeated blocking in a parallel
      // loop because it can cause the runtime to create 
      // a large number of threads over a short period of time.
      send(buffer, i);
   });
}

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

[в начало]

Не выполняйте операции блокировки при отмене параллельных задач

При возможности не выполняйте операции блокировки до отмены параллельной работы вызовом метода Concurrency::task_group::cancel или Concurrency::structured_task_group::cancel.

Когда задача выполняет операцию блокировки, среда выполнения может выполнять другую работу, пока первая задача ожидает получения данных. При включении планирования в режиме пользователя (UMS) среда выполнения производит другую работу, когда задача выполняет операцию совместной блокировки или операцию блокировки, включающую переход в режим ядра. При обычном планировании потоков (используется по умолчанию) среда выполнения производит другую работу, только когда задача выполняет операцию совместной блокировки. После разблокирования среда выполнения заново планирует ожидающую задачу. Среда выполнения обычно планирует задачи, разблокированные недавно, раньше, чем задачи, разблокированные давно. Поэтому во время операции блокирования среда выполнения может запланировать лишнюю работу, что уменьшает производительность. Таким образом, при выполнении операции блокирования до отмены параллельной работы операция блокирования может задержать вызов cancel. Из-за этого другие задачи выполняют лишнюю работу.

Рассмотрим следующий пример, определяющий функцию parallel_find_answer, которая выполняет в предоставленном массиве поиск элемента, удовлетворяющего заданной функции предиката. Когда функция предиката возвращает значение true, параллельная рабочая функция создает объект Answer и отменяет общую задачу.

// blocking-cancel.cpp
// compile with: /c /EHsc
#include <windows.h>
#include <ppl.h>

using namespace Concurrency;

// Encapsulates the result of a search operation.
template<typename T>
class Answer
{
public:
   explicit Answer(const T& data)
      : _data(data)
   {
   }

   T get_data() const
   {
      return _data;
   }

   // TODO: Add other methods as needed.

private:
   T _data;

   // TODO: Add other data members as needed.
};

// Searches for an element of the provided array that satisfies the provided
// predicate function.
template<typename T, class Predicate>
Answer<T>* parallel_find_answer(const T a[], size_t count, const Predicate& pred)
{
   // The result of the search.
   Answer<T>* answer = nullptr;
   // Ensures that only one task produces an answer.
   volatile long first_result = 0;

   // Use parallel_for and a task group to search for the element.
   structured_task_group tasks;
   tasks.run_and_wait([&]
   {
      // Declare the type alias for use in the inner lambda function.
      typedef T T;

      parallel_for<size_t>(0, count, [&](const T& n) {
         if (pred(a[n]) && InterlockedExchange(&first_result, 1) == 0)
         {
            // Create an object that holds the answer.
            answer = new Answer<T>(a[n]);
            // Cancel the overall task.
            tasks.cancel();
         }
      });
   });

   return answer;
}

Оператор new выполняет выделение памяти в куче, эта операция может заблокироваться. Если включено планирование в режиме пользователя (UMS), среда выполнения производит другую работу во время операции блокирования. При обычном планировании потоков среда выполнения производит другую работу, только когда задача выполняет вызов совместной блокировки, например вызов Concurrency::critical_section::lock.

В следующем примере показано, как предотвратить лишнюю работу и увеличить производительность. Этот пример отменяет группу задач до выделения хранилища для объекта Answer.

// Searches for an element of the provided array that satisfies the provided
// predicate function.
template<typename T, class Predicate>
Answer<T>* parallel_find_answer(const T a[], size_t count, const Predicate& pred)
{
   // The result of the search.
   Answer<T>* answer = nullptr;
   // Ensures that only one task produces an answer.
   volatile long first_result = 0;

   // Use parallel_for and a task group to search for the element.
   structured_task_group tasks;
   tasks.run_and_wait([&]
   {
      // Declare the type alias for use in the inner lambda function.
      typedef T T;

      parallel_for<size_t>(0, count, [&](const T& n) {
         if (pred(a[n]) && InterlockedExchange(&first_result, 1) == 0)
         {
            // Cancel the overall task.
            tasks.cancel();
            // Create an object that holds the answer.
            answer = new Answer<T>(a[n]);            
         }
      });
   });

   return answer;
}

[в начало]

Не выполняйте запись в общие данные в параллельном цикле

Среда выполнения с параллелизмом предоставляет несколько структур данных, например Concurrency::critical_section, синхронизирующих параллельный доступ к общим данным. Эти структуры данных часто бывают полезны, например если нескольким задачам иногда нужен совместный доступ к ресурсу.

Рассмотрим пример, в котором алгоритм Concurrency::parallel_for_each и объект critical_section используются для вычисления количества простых чисел в объекте std::array. Этот пример не является масштабируемым, так как каждый поток должен ждать, пока станет доступна общая переменная prime_sum.

critical_section cs;
prime_sum = 0;
parallel_for_each(a.begin(), a.end(), [&](int i) {
   cs.lock();
   prime_sum += (is_prime(i) ? i : 0);
   cs.unlock();
});

Этот пример приводит к низкой производительности также потому, что часто проводимая операция блокирования по существую делает цикл последовательным. Кроме того, когда объект среды выполнения с параллелизмом выполняет операцию блокирования, планировщик может создать дополнительный поток, чтобы выполнять другую работу, пока первый поток ожидает поступления данных. Если среда выполнения создает много потоков, потому что много задач ожидает доступа к общим данным, производительность приложения может упасть или оно может оказаться в состоянии дефицита ресурсов.

Библиотека PPL определяет класс Concurrency::combinable, который помогает исключить состояние с общим доступом, предоставляя доступ к общим ресурсам без блокировок. Класс combinable предоставляет локальную память потока, позволяющую выполнять детализированные вычисления и объединять их результаты в общий результат. Объект combinable можно сравнить с переменной сокращения.

Следующий пример, в отличие от предыдущего, использует для вычисления суммы объект combinable вместо объекта critical_section. Этот пример масштабируется, так как каждый поток содержит свою локальную копию суммы. В этом примере для объединения вычислений в конечный результат используется метод Concurrency::combinable::combine.

combinable<int> sum;
parallel_for_each(a.begin(), a.end(), [&](int i) {
   sum.local() += (is_prime(i) ? i : 0);
});
prime_sum = sum.combine(plus<int>());

Полную версию этого примера см. в разделе Практическое руководство. Использование класса combinable для повышения производительности. Дополнительные сведения о классе combinable см. в разделе Параллельные контейнеры и объекты.

[в начало]

По возможности избегайте ложного совместного использования

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

В следующем базовом примере показаны две параллельные задачи, увеличивающие общую переменную счетчика.

volatile long count = 0L;
Concurrency::parallel_invoke(
   [&count] {
      for(int i = 0; i < 100000000; ++i)
         InterlockedIncrement(&count);
   },
   [&count] {
      for(int i = 0; i < 100000000; ++i)
         InterlockedIncrement(&count);
   }
);

Чтобы исключить совместное использование данных двумя задачами, можно изменить пример так, чтобы он использовал две переменные счетчика. Этот пример вычисляет окончательное значение счетчика после выполнения задач. Тем не менее этот пример иллюстрирует ложное совместное использование, потому что переменные count1 и count2 расположены, вероятно, в одной строке кэша.

long count1 = 0L;
long count2 = 0L;
Concurrency::parallel_invoke(
   [&count1] {
      for(int i = 0; i < 100000000; ++i)
         ++count1;
   },
   [&count2] {
      for(int i = 0; i < 100000000; ++i)
         ++count2;
   }
);
long count = count1 + count2;

Один из способов исключить ложное совместное использование — следить, чтобы переменные счетчиков были в разных строках кэша. В следующем примере переменные count1 и count2 выровнены по 64-байтовым границам.

__declspec(align(64)) long count1 = 0L;      
__declspec(align(64)) long count2 = 0L;      
Concurrency::parallel_invoke(
   [&count1] {
      for(int i = 0; i < 100000000; ++i)
         ++count1;
   },
   [&count2] {
      for(int i = 0; i < 100000000; ++i)
         ++count2;
   }
);
long count = count1 + count2;

Этот пример предполагает, что размер кэша памяти — 64 байта или менее.

Если задачи должны использовать данные совместно, рекомендуется использовать класс Concurrency::combinable. Класс combinable создает локальные переменные потока так, что ложное совместное использование менее вероятно. Дополнительные сведения о классе combinable см. в разделе Параллельные контейнеры и объекты.

[в начало]

Следите, чтобы переменные были допустимы в течение всего времени существования задачи

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

Рассмотрим следующий пример, определяющий класс object и функцию perform_action. Функция perform_action создает переменную object и выполняет некоторые действия с этой переменной асинхронно. Так как задача может не завершиться до выполнения возврата функцией perform_action, произойдет аварийное завершение программы или программа может вести себя неопределенным образом, если переменная object будет уничтожена во время выполнения задачи.

// lambda-lifetime.cpp
// compile with: /c /EHsc
#include <ppl.h>

using namespace Concurrency;

// A type that performs an action.
class object
{
public:
   void action() const
   {
      // TODO: Details omitted for brevity.
   }
};

// Performs an action asynchronously.
void perform_action(task_group& tasks)
{
   // Create an object variable and perform some action on 
   // that variable asynchronously.
   object obj;
   tasks.run([&obj] {
      obj.action();
   });

   // NOTE: The object variable is destroyed here. The program
   // will crash or exhibit unspecified behavior if the task
   // is still running when this function returns.
}

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

В следующем примере переменная object передается задаче по значению. Поэтому задача работает с собственной копией переменной.

// Performs an action asynchronously.
void perform_action(task_group& tasks)
{
   // Create an object variable and perform some action on 
   // that variable asynchronously.
   object obj;
   tasks.run([obj] {
      obj.action();
   });
}

Так как переменная передается object по значению, изменения состояния этой переменной не влияют на исходную копию.

В следующем примере метод Concurrency::task_group::wait используется, чтобы задача завершилась до выполнения возврата функцией perform_action.

// Performs an action.
void perform_action(task_group& tasks)
{
   // Create an object variable and perform some action on 
   // that variable.
   object obj;
   tasks.run([&obj] {
      obj.action();
   });

   // Wait for the task to finish. 
   tasks.wait();
}

Так как задача завершается до выполнения возврата функцией, функция perform_action не ведет себя асинхронно.

В следующем примере функция perform_action изменяется так, чтобы принимать ссылку на переменную object. Вызывающая сторона должна следить, чтобы переменная object существовала до завершения задачи.

// Performs an action asynchronously.
void perform_action(object& obj, task_group& tasks)
{
   // Perform some action on the object variable.
   tasks.run([&obj] {
      obj.action();
   });
}

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

Дополнительные сведения о лямбда-выражениях см. в разделе Lambda Expressions in C++.

[в начало]

См. также

Задачи

Практическое руководство. Использование функции parallel_invoke для написания программы параллельной сортировки

Практическое руководство. Использование отмены для выхода из параллельного цикла

Практическое руководство. Использование класса combinable для повышения производительности

Основные понятия

Рекомендации по работе со средой выполнения с параллелизмом

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

Параллельные контейнеры и объекты

Параллельные алгоритмы

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

Обработка исключений в среде выполнения с параллелизмом

Другие ресурсы

Пошаговое руководство. Создание сети обработки изображений

Рекомендации по работе с библиотекой асинхронных агентов

Общие рекомендации в среде выполнения с параллелизмом

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

Дата

Журнал

Причина

Март 2011

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

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

Май 2010

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

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