Compartir a través de


Procedimientos recomendados en la biblioteca de modelos paralelos

En este documento se describe la mejor forma de usar eficazmente la biblioteca de patrones de procesamiento paralelos (PPL). PPL proporciona algoritmos, objetos y contenedores de propósito general para realizar paralelismos específicos.

Para obtener más información sobre la PPL, consulte Biblioteca de modelos de procesamiento paralelo (PPL).

Secciones

Este documento contiene las siguientes secciones:

No paralelizar cuerpos de bucle pequeño

La ejecución en paralelo de cuerpos de bucle relativamente pequeños puede hacer que la sobrecarga de programación asociada supere a las ventajas del procesamiento en paralelo. Considere el ejemplo siguiente, que agrega cada par de elementos en dos matrices.

// 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.
}

La carga de trabajo de cada iteración del bucle paralelo es demasiado pequeña para poder obtener beneficios de la sobrecarga de procesamiento en paralelo. Puede mejorar el rendimiento de este bucle realizando más trabajo en el cuerpo del bucle o realizando el bucle en serie.

[Arriba]

Paralelismo rápido en el nivel más alto posible

Cuando se paraleliza código solamente en el nivel inferior, se puede introducir una construcción del tipo bifurcar-combinar que no crece cuando aumenta el número de procesadores. Una construcción del tipo bifurcar-recombinar es una construcción en la que una tarea divide su trabajo en subtareas menores paralelas y espera a que finalicen esas subtareas. Cada subtarea puede dividirse de forma recursiva en subtareas adicionales.

Aunque el modelo del tipo bifurcar-recombinar puede ser útil para resolver diversos problemas, hay situaciones en las que la sobrecarga de la sincronización puede disminuir la escalabilidad. Por ejemplo, considere el siguiente código en serie que procesa datos de imagen.

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

Dado que cada iteración del bucle es independiente, puede paralelizar buena parte del trabajo, como se muestra en el ejemplo siguiente. Este ejemplo utiliza el algoritmo concurrency::parallel_for para paralelizar el bucle exterior.

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

El ejemplo siguiente muestra una construcción del tipo bifurcar-recombinar llamando a la función ProcessImage en un bucle. Cada llamada a ProcessImage no se resuelve hasta que finaliza cada subtarea.

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

Si el trabajo que realiza cada iteración del bucle paralelo es casi inexistente o el trabajo que realiza el bucle paralelo no está equilibrado (es decir, algunas iteraciones del bucle tardan más que otras), la sobrecarga de programación que se necesita para bifurcar y recombinar el trabajo con tanta frecuencia puede sobrepasar el beneficio obtenido con la ejecución en paralelo. Esta sobrecarga aumenta cuando aumenta el número de procesadores.

Para reducir la cantidad de sobrecarga de programación en este ejemplo, puede paralelizar los bucles externos antes de paralelizar los bucles internos, o bien utilizar otra construcción paralela, como una canalización. En el ejemplo siguiente se modifica la función ProcessImages para usar el algoritmo concurrency::parallel_for_each y paralelizar el bucle exterior.

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

Para ver un ejemplo similar que usa una canalización para realizar el procesamiento de imágenes en paralelo, consulte Tutorial: Crear una red de procesamiento de imagen.

[Arriba]

Usar parallel_invoke para resolver problemas de división y conquista

Un problema divide y vencerás es una forma de la construcción bifurcar-recombinar que usa la recursividad para dividir una tarea en subtareas. Además de las clases concurrency::task_group y concurrency::structured_task_group, también puede utilizar el algoritmo concurrency::parallel_invoke para solucionar problemas de tipo divide y vencerás. El algoritmo parallel_invoke tiene una sintaxis más concisa que los objetos de grupo de tareas y es útil cuando se tiene un número fijo de tareas paralelas.

En el ejemplo siguiente se muestra el uso del algoritmo parallel_invoke para implementar el algoritmo de ordenación bitónica.

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

Para reducir la sobrecarga, el algoritmo parallel_invoke realiza la última serie de tareas en el contexto de la llamada.

Para obtener la versión completa de este ejemplo, consulte Cómo usar parallel_invoke para escribir una rutina de ordenación paralela. Para más información sobre el algoritmo parallel_invoke, vea Algoritmos paralelos.

[Arriba]

Usar el control de excepciones o cancelación para interrumpir un bucle paralelo

PPL proporciona dos mecanismos para cancelar el trabajo paralelo que realiza un algoritmo paralelo o un grupo de tareas. Uno de ellos consiste en utilizar el mecanismo de cancelación proporcionada por las clases concurrency::task_group y concurrency::structured_task_group. El otro mecanismo consiste en producir una excepción en el cuerpo de la función de trabajo de una tarea. El mecanismo de cancelación es más eficaz que el control de excepciones para cancelar un árbol de trabajo paralelo. Un árbol de trabajo paralelo es un grupo de grupos de tareas relacionadas en el que algunos grupos de tareas contienen otros grupos de tareas. El mecanismo de cancelación cancela un grupo de tareas y sus grupos de tareas secundarios de forma descendente. Por el contrario, el control de excepciones funciona de manera ascendente y debe cancelar cada grupo de tareas secundario por separado a medida que la excepción se propaga hacia arriba.

Cuando trabaje directamente con un objeto de grupo de tareas, utilice el método concurrency::task_group::cancel o el método concurrency::structured_task_group::cancel para cancelar el trabajo que corresponde a ese grupo de tareas. Para cancelar un algoritmo paralelo, por ejemplo, parallel_for, cree un grupo de tareas primario y cancele ese grupo de tareas. Por ejemplo, considere la siguiente función, parallel_find_any, que busca un valor en una matriz en paralelo.

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

Dado que los algoritmos paralelos usan grupos de tareas, cuando una de las iteraciones paralelas cancela el grupo de tareas primario, se cancela la tarea global. Para obtener la versión completa de este ejemplo, consulte Cómo usar la cancelación para interrumpir un bucle paralelo.

Aunque el control de excepciones es una forma menos eficaz de cancelar el trabajo paralelo que el mecanismo de cancelación, hay casos en los que el control de excepciones es adecuado. Por ejemplo, el siguiente método, for_all, realiza de forma recursiva una función de trabajo en cada nodo de una estructura tree. En este ejemplo, el miembro de datos _children es una std::list que contiene objetos 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);
}

El llamador del método tree::for_all puede producir una excepción si no requiere que se llame a la función de trabajo en cada elemento del árbol. El ejemplo siguiente se muestra la función search_for_value, que busca un valor en el objeto tree proporcionado. La función search_for_value usa una función de trabajo que produce una excepción cuando el elemento actual del árbol coincide con el valor proporcionado. La función search_for_value usa un bloque try-catch para capturar la excepción e imprimir el resultado en la consola.

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

Para obtener la versión completa de este ejemplo, consulte Cómo usar el control de excepciones para interrumpir un bucle paralelo.

Para más información general sobre los mecanismos de control de excepciones y cancelación que proporciona la PPL, consulte y Control de excepciones en el runtime de simultaneidad.Cancelación en la biblioteca PPL y Control de excepciones.

[Arriba]

Comprender cómo afectan la cancelación y el control de excepciones a la destrucción de objetos

En un árbol de trabajo paralelo, una tarea que se cancela impide que se ejecuten las tareas secundarias. Esto puede causar problemas si una de las tareas secundarias realiza una operación que tiene importancia para la aplicación, como liberar un recurso. Además, la cancelación de tareas puede hacer que una excepción se propague a través de un destructor de objeto y provoque un comportamiento no definido en la aplicación.

En el ejemplo siguiente, la clase Resource describe un recurso y la clase Container describe un contenedor que contiene los recursos. En su destructor, la clase Container llama al cleanup método en dos de sus miembros Resource en paralelo y, a continuación, llama al método cleanup en su tercer miembro 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;
};

Aunque este patrón no tiene ningún problema por sí mismo, considere el siguiente código, que ejecuta dos tareas en paralelo. La primera tarea crea un objeto Container y la segunda tarea cancela la tarea global. Con carácter ilustrativo, el ejemplo utiliza dos objetos concurrency::event para asegurarse de que la cancelación se produce después de crear el objeto Container y de que el objeto Container se destruye después de realizar la operación de cancelación.

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

Este ejemplo produce el siguiente resultado:

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

Este ejemplo de código presenta los siguientes problemas que pueden hacer que no se comporte de la forma esperada:

  • La cancelación de la tarea principal ocasiona que la tarea secundaria (la llamada a concurrency::parallel_invoke) se cancele también. Por lo tanto, estos dos recursos no se liberan.

  • La cancelación de la tarea principal hace que la tarea secundaria produzca una excepción interna. Dado que el destructor Container no controla esta excepción, la excepción se propaga hacia arriba y no se libera el tercer recurso.

  • La excepción producida por la tarea secundaria se propaga a través del destructor Container. Al propagarse desde un destructor, pone a la aplicación en un estado indefinido.

Se recomienda que no se realicen operaciones críticas, como liberar recursos, en las tareas, a menos que pueda garantizar que esas tareas no se cancelarán. También se recomienda no utilizar funcionalidad de tiempo de ejecución que pueda introducir el destructor de los tipos.

[Arriba]

No bloquear repetidamente en un bucle paralelo

Un bucle paralelo, como concurrency::parallel_for o concurrency::parallel_for_each, que esté dominado por operaciones de bloqueo, puede ocasionar que el runtime cree muchos subprocesos en poco tiempo.

El Runtime de simultaneidad realiza un trabajo adicional cuando una tarea finaliza, o cuando bloquea o produce un resultado de forma cooperativa. Cuando una iteración de un bucle paralelo se bloquea, el runtime podría iniciar otra iteración. Si no hay ningún subproceso inactivo disponible, el runtime crea un subproceso nuevo.

Cuando el cuerpo de un bucle paralelo se bloquea puntualmente, este mecanismo ayuda a maximizar el rendimiento de la tarea global. Sin embargo, cuando se bloquean muchas iteraciones, el runtime puede crear muchos subprocesos para ejecutar el trabajo adicional. Esto podría provocar un estado de memoria insuficiente o una mala utilización de los recursos de hardware.

Considere el siguiente ejemplo, que llama a la función concurrency::send en cada iteración de un bucle parallel_for. Dado que send bloquea de forma cooperativa, el runtime crea un nuevo subproceso para ejecutar el trabajo adicional cada vez que se llama a 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);
   });
}

Se recomienda refactorizar el código para evitar este patrón. En este ejemplo, puede evitar la creación de subprocesos adicionales mediante una llamada a send en un bucle for en serie.

[Arriba]

No realizar operaciones de bloqueo al cancelar el trabajo paralelo

Cuando sea posible, evite realizar operaciones de bloqueo antes de llamar a al métodoconcurrency::task_group::cancel o al método concurrency::structured_task_group::cancel para cancelar el trabajo paralelo.

Cuando una tarea realiza una operación de bloqueo cooperativa, el runtime puede realizar otro trabajo mientras la primera tarea espera los datos. El runtime reprograma la tarea en espera cuando se desbloquea. Normalmente, el runtime reprograma las tareas que se han desbloqueado más recientemente antes de reprogramar las tareas que se han desbloqueado menos recientemente. El runtime, por tanto, podría programar trabajo innecesario durante la operación de bloqueo, lo que provocaría una disminución del rendimiento. En consecuencia, si se realiza una operación de bloqueo antes de cancelar el trabajo paralelo, la operación de bloqueo puede retrasar la llamada a cancel. Esto hace que otras tareas realicen trabajo innecesario.

Considere el siguiente ejemplo en el que se define la función parallel_find_answer, que busca un elemento de la matriz proporcionada que cumpla la función de predicado proporcionada. Cuando la función de predicado devuelve true, la función de trabajo paralela crea un objeto Answer y cancela la tarea general.

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

El operador new realiza una asignación en el montón, lo que podría producir un bloqueo. El runtime realiza otro trabajo solo cuando la tarea realiza una llamada de bloqueo cooperativo, como una llamada a concurrency::critical_section::lock.

En el ejemplo siguiente se muestra cómo evitar el trabajo innecesario y mejorar así el rendimiento. Este ejemplo cancela el grupo de tareas antes de asignar el almacenamiento para el objeto 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;
}

[Arriba]

No escribir en datos compartidos en un bucle paralelo

El Runtime de simultaneidad proporciona varias estructuras de datos; por ejemplo, concurrency::critical_section, que sincronizan el acceso simultáneo a los datos compartidos. Estas estructuras de datos son útiles en muchos casos; por ejemplo, cuando varias tareas requieren con poca frecuencia acceso compartido a un recurso.

Considere el siguiente ejemplo, que utiliza el algoritmo concurrency::parallel_for_each y un objeto critical_section para calcular el recuento de números primos en un objeto std::array. Este ejemplo no se escala porque cada subproceso debe esperar para obtener acceso a la variable compartida 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();
});

Este ejemplo también puede provocar un rendimiento bajo porque la operación de bloqueo frecuente serializa eficazmente el bucle. Además, cuando un objeto del Runtime de simultaneidad realiza una operación de bloqueo, el programador puede crear un subproceso adicional que realice otro trabajo mientras el primer subproceso espera los datos. Si el tiempo de ejecución crea muchos subprocesos porque hay muchas tareas esperando los datos compartidos, la aplicación puede tener un rendimiento bajo o entrar en un estado de escasez de recursos.

La PPL define la clase concurrency::combinable, que ayuda a eliminar el estado compartido al proporcionar acceso a los recursos compartidos sin utilizar bloqueos. La clase combinable proporciona un almacenamiento local para los subprocesos que permite realizar cálculos específicos y, a continuación, combinar estos cálculos en un resultado final. Puede pensar en un objeto combinable como una variable de reducción.

El ejemplo siguiente modifica el ejemplo anterior mediante el uso de un objeto combinable en lugar de un objeto critical_section para calcular la suma. Este ejemplo sí se escala, porque cada subproceso tiene su propia copia local de la suma. Este ejemplo utiliza el método concurrency::combinable::combine para fusionar mediante los cálculos locales en el resultado final.

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>());

Para obtener la versión completa de este ejemplo, consulte Procedimiento para usar la clase combinable para mejorar el rendimiento. Para más información sobre la clase combinable, consulte Contenedores y objetos paralelos.

[Arriba]

Siempre que sea posible, evite el uso compartido falso.

El uso compartido falso se produce cuando varias tareas simultáneas que se ejecutan en procesadores separados escriben en variables que se encuentran en la misma línea de caché. Cuando una tarea escribe en una de las variables, se invalida la línea de caché de las dos variables. Cada procesador debe volver a cargar la línea de caché cada vez que esta se invalida. Por lo tanto, el uso compartido falso puede causar una disminución del rendimiento de la aplicación.

El siguiente ejemplo básico se muestra dos tareas simultáneas, cada una de las cuales incrementa una variable de contador compartida.

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

Para eliminar el uso compartido de datos entre las dos tareas, se puede modificar el ejemplo de forma que use dos variables de contador. Este ejemplo calcula el valor final del contador después de finalizar las tareas. Sin embargo, este es un ejemplo de uso compartido falso, porque es muy probable que las variables count1 y count2 se encuentren en la misma línea de caché.

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;

Una manera de eliminar el uso compartido falso consiste en asegurarse de que las variables de contador estén en líneas de caché distintas. El ejemplo siguiente alinea las variables count1 y count2 en límites de 64 bytes.

__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;

En este ejemplo se supone que el tamaño de la memoria caché es de 64 bytes o inferior.

Se recomienda utilizar la clase concurrency::combinable cuando deba compartir datos entre tareas. La clase combinable crea variables locales para los subprocesos de tal manera que es menos probable que se dé un uso compartido falso. Para más información sobre la clase combinable, consulte Contenedores y objetos paralelos.

[Arriba]

Asegúrese de que las variables son válidas durante toda la duración de una tarea

Al proporcionar una expresión lambda a un grupo de tareas o algoritmo paralelo, la cláusula de captura especifica si el cuerpo de la expresión lambda tiene acceso a las variables del ámbito de inclusión por valor o por referencia. Cuando se pasan variables por referencia a una expresión lambda, es necesario garantizar que esas variables van a persistir hasta que la tarea finalice.

Considere el ejemplo siguiente que define la clase object y la función perform_action. La función perform_action crea una variable object y realiza alguna acción en esa variable de forma asincrónica. Dado que no se garantiza que la tarea finalice antes de que la función perform_action se resuelva, el programa se bloqueará o mostrará un comportamiento no especificado si la variable object se destruye mientras se ejecuta la tarea.

// 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.
}

Según los requisitos de la aplicación, puede utilizar una de las siguientes técnicas para asegurarse de que las variables sigan siendo válidas mientras dure cualquiera de las tareas.

En el ejemplo siguiente se pasa la variable object por valor a la tarea. Por lo tanto, la tarea opera sobre su propia copia de la variable.

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

Dado que la variable object se pasa por valor, cualquier cambio de estado que se produzca en esta variable no aparecerá en la copia original.

En el ejemplo siguiente se utiliza el método concurrency::task_group::wait para asegurarse de que la tarea finalice antes de que la función perform_action se resuelva.

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

Dado que ahora finaliza la tarea antes de que se resuelva la función, la función perform_action ya no se comporta de forma asincrónica.

En el ejemplo siguiente se modifica la función perform_action para que tome una referencia a la variable object. El llamador debe garantizar que la duración de la variable object sea válida hasta que finalice la tarea.

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

También puede utilizar un puntero para controlar la duración del objeto que va a pasar a un algoritmo paralelo o un grupo de tareas.

Para obtener más información sobre las expresiones lambda, vea Expresiones lambda.

[Arriba]

Consulte también

Procedimientos recomendados del Runtime de simultaneidad
Biblioteca de modelos de procesamiento paralelo (PPL)
Contenedores y objetos paralelos
Algoritmos paralelos
Cancelación en la biblioteca PPL
Control de excepciones
Tutorial: Crear una red de procesamiento de imagen
Procedimiento para usar parallel.invoke para escribir una rutina de ordenación en paralelo
Procedimiento para usar la cancelación para interrumpir un bucle Parallel
Procedimiento para usar la clase combinable para mejorar el rendimiento
Procedimientos recomendados en la biblioteca de agentes asincrónicos
Procedimientos recomendados generales en el Runtime de simultaneidad