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

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

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

Разделы

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

Не параллелизируйте небольшие тела цикла

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

// 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);
}

Поскольку каждая итерация цикла независима, можно распараллеливать большую часть работы, как показано в следующем примере. В этом примере используется алгоритм параллелизма::p arallel_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(begin(bitmaps), end(bitmaps), [&f](Bitmap* bmp) {
      ProcessImage(bmp, f);
   });
}

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

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

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

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

[В начало]

Использование parallel_invoke для решения проблем разделения и завоевания

Проблема деления и завоевания — это форма конструкции соединения вилки, которая использует рекурсию для разрыва задачи на подзадачи. Помимо классов параллелизма::task_group и параллелизма::structured_task_group, можно также использовать алгоритм параллелизма::p arallel_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 предоставляет два способа отмены параллельной работы, выполняемой группой задач или параллельным алгоритмом. Одним из способов является использование механизма отмены, предоставляемого классами параллелизма::task_group и параллелизма::structured_task_group . Второй способ — создать исключение в теле рабочей функции задачи. Механизм отмены более эффективен, чем обработка исключений при отмене дерева параллельной работы. Параллельное дерево работы — это группа связанных групп задач, в которых некоторые группы задач содержат другие группы задач. Механизм отмены отменяет группу задач и ее дочерние группы в порядке «сверху вниз». И наоборот, обработка исключений работает в режиме «снизу вверх» и необходимо отменять каждую дочернюю группу задач независимо, поскольку исключение распространяется вверх.

При работе непосредственно с объектом группы задач используйте методы параллелизма::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;

   // Call parallel_for in the context of a cancellation token to search for the element.
   cancellation_token_source cts;
   run_with_cancellation_token([count, what, &a, &position, &cts]()
   {
      parallel_for(std::size_t(0), count, [what, &a, &position, &cts](int n) {
         if (a[n] == what)
         {
            // Set the return value and cancel the remaining tasks.
            position = n;
            cts.cancel();
         }
      });
   }, cts.get_token());

   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(begin(_children), end(_children), [&](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, см. в разделе "Отмена" в 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, а вторая задача отменяет общую задачу. Для иллюстрации в примере используется два объекта параллелизма::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...

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

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

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

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

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

[В начало]

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

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

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

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

Рассмотрим следующий пример, который вызывает функцию параллелизма::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.

[В начало]

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

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

Когда задача выполняет операцию совместной блокировки, среда выполнения может выполнять другую работу, пока первая задача ожидает получения данных. Среда выполнения переносит выполнение ожидающей задачи на момент после разблокирования. Как правило, среда выполнения сначала переносит выполнение задач, разблокированных недавно, а затем — задач, разблокированных ранее. Поэтому среда выполнения может запланировать лишнюю работу во время операции блокировки, что приводит к снижению производительности. Соответственно, при выполнении операции блокировки до отмены параллельной работы операция блокировки может задержать вызов метода 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 выполняет выделение кучи, которое может блокироваться. Среда выполнения выполняет другую работу, только если задача выполняет совместный вызов блокировки, например вызов параллелизма::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;
}

[В начало]

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

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

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

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

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

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

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

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

Полный вариант этого примера см. в разделе "Практическое руководство. Использование объединения для повышения производительности". Дополнительные сведения о классе см. в разделе "Параллельные 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 байта или менее.

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

В следующем примере используется метод параллелизма::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();
   });
}

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

Дополнительные сведения о лямбда-выражениях см. в разделе Лямбда-выражения.

[В начало]

См. также

Рекомендации по работе со средой выполнения с параллелизмом
Библиотека параллельных шаблонов
Параллельные контейнеры и объекты
Параллельные алгоритмы
Отмена в библиотеке параллельных шаблонов
Обработка исключений
Пошаговое руководство. Создание сети обработки изображений
Практическое руководство. Использование функции parallel_invoke для написания программы параллельной сортировки
Практическое руководство. Использование отмены для выхода из параллельного цикла
Практическое руководство. Использование класса combinable для повышения производительности
Рекомендации по работе с библиотекой асинхронных агентов
Общие рекомендации в среде выполнения с параллелизмом