Parallel Patterns Library (PPL)

La Biblioteca de patrones de procesamiento paralelo (PPL) proporciona un modelo de programación imperativo que favorece la escalabilidad y facilidad de uso para desarrollar aplicaciones simultáneas. PPL se basa en los componentes de administración de recursos y programación del Runtime de simultaneidad. Eleva el nivel de abstracción entre el código de aplicación y el mecanismo de subprocesamiento subyacente proporcionando contenedores y algoritmos genéricos, con seguridad de tipos, que actúan sobre los datos en paralelo. PPL también permite desarrollar aplicaciones que escalan proporcionando alternativas al estado compartido.

PPL proporciona las características siguientes.

  • Paralelismo de tareas: un mecanismo que se basa en el grupo de subprocesos de Windows para ejecutar varios elementos de trabajo (tareas) en paralelo

  • Algoritmos paralelos: algoritmos genéricos basados en el Runtime de simultaneidad que actúan en colecciones de datos en paralelo

  • Contenedores y objetos paralelos: tipos de contenedores genéricos que proporcionan acceso simultáneo seguro a sus elementos

Ejemplo

La PPL proporciona un modelo de programación que se parece al de la Biblioteca estándar de C++. En el ejemplo siguiente se muestran muchas características de PPL: Calcula varios números de Fibonacci en serie y en paralelo. Ambos cálculos actúan sobre un objeto std::array. También se imprime en la consola el tiempo necesario para realizar ambos cálculos.

La versión en serie usa el algoritmo std::for_each de la Biblioteca estándar para atravesar la matriz y almacena los resultados en un objeto std::vector. La versión en paralelo realiza la misma tarea, pero usa el algoritmo concurrency::parallel_for_each de PPL y almacena los resultados en un objeto concurrency::concurrent_vector. La clase concurrent_vector permite que cada iteración del bucle agregue los elementos de forma simultánea, sin el requisito de sincronizar el acceso de escritura en el contenedor.

Dado que parallel_for_each actúa de manera simultánea, la versión del cálculo en paralelo de este ejemplo debe ordenar el objeto concurrent_vector para generar los mismos resultados que la versión del cálculo en serie.

Observe que en el ejemplo se usa un método sencillo para calcular los números de Fibonacci; sin embargo, este método muestra cómo el Runtime de simultaneidad puede mejorar el rendimiento en el caso de cálculos largos.

// parallel-fibonacci.cpp
// compile with: /EHsc
#include <windows.h>
#include <ppl.h>
#include <concurrent_vector.h>
#include <array>
#include <vector>
#include <tuple>
#include <algorithm>
#include <iostream>

using namespace concurrency;
using namespace std;

// Calls the provided work function and returns the number of milliseconds 
// that it takes to call that function.
template <class Function>
__int64 time_call(Function&& f)
{
   __int64 begin = GetTickCount();
   f();
   return GetTickCount() - begin;
}

// Computes the nth Fibonacci number.
int fibonacci(int n)
{
   if(n < 2)
      return n;
   return fibonacci(n-1) + fibonacci(n-2);
}

int wmain()
{
   __int64 elapsed;

   // An array of Fibonacci numbers to compute.
   array<int, 4> a = { 24, 26, 41, 42 };

   // The results of the serial computation.
   vector<tuple<int,int>> results1;

   // The results of the parallel computation.
   concurrent_vector<tuple<int,int>> results2;

   // Use the for_each algorithm to compute the results serially.
   elapsed = time_call([&] 
   {
      for_each (begin(a), end(a), [&](int n) {
         results1.push_back(make_tuple(n, fibonacci(n)));
      });
   });   
   wcout << L"serial time: " << elapsed << L" ms" << endl;
   
   // Use the parallel_for_each algorithm to perform the same task.
   elapsed = time_call([&] 
   {
      parallel_for_each (begin(a), end(a), [&](int n) {
         results2.push_back(make_tuple(n, fibonacci(n)));
      });

      // Because parallel_for_each acts concurrently, the results do not 
      // have a pre-determined order. Sort the concurrent_vector object
      // so that the results match the serial version.
      sort(begin(results2), end(results2));
   });   
   wcout << L"parallel time: " << elapsed << L" ms" << endl << endl;

   // Print the results.
   for_each (begin(results2), end(results2), [](tuple<int,int>& pair) {
      wcout << L"fib(" << get<0>(pair) << L"): " << get<1>(pair) << endl;
   });
}

La siguiente salida de ejemplo corresponde a un equipo con cuatro procesadores.

serial time: 9250 ms
parallel time: 5726 ms

fib(24): 46368
fib(26): 121393
fib(41): 165580141
fib(42): 267914296

Cada iteración del bucle requiere una cantidad de tiempo diferente para finalizar. La operación que finaliza en último lugar limita el rendimiento de parallel_for_each. Por consiguiente, no debería esperar mejoras de rendimiento lineales entre las versiones en serie y en paralelo de este ejemplo.

Title Descripción
Paralelismo de tareas Describe el rol de las tareas y los grupos de tareas en la biblioteca PPL.
Algoritmos paralelos Se describe cómo usar algoritmos paralelos, como parallel_for y parallel_for_each.
Contenedores y objetos paralelos Se describen los distintos objetos y contenedores paralelos que proporciona PPL.
Cancelación en la biblioteca PPL Explica cómo cancelar el trabajo realizado por un algoritmo paralelo.
Runtime de simultaneidad Se describe el Runtime de simultaneidad, que simplifica la programación en paralelo, y contiene vínculos a los temas relacionados.