Parallel Patterns Library (PPL)

La Biblioteca de modelos 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 para ejecutar varios elementos de trabajo (tareas) en paralelo.

  • Algoritmos paralelos: algoritmos genéricos que actúan sobre colecciones de datos en paralelo.

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

Ejemplo

PPL proporciona un modelo de programación que se parece al de la Biblioteca de plantillas estándar (STL). 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 del cálculo en serie usa el algoritmo std::for_each de STL para atravesar la matriz y almacena los resultados en un objeto std::vector. La versión del cálculo 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 (a.begin(), a.end(), [&](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 (a.begin(), a.end(), [&](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(results2.begin(), results2.end());
   });   
   wcout << L"parallel time: " << elapsed << L" ms" << endl << endl;

   // Print the results.
   for_each (results2.begin(), results2.end(), [](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.

Temas relacionados

Título

Descripción

Paralelismo de tareas (Runtime de simultaneidad)

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 temas relacionados.