Parallel Patterns Library (PPL)

Die Parallel Patterns Library (PPL) stellt ein obligatorisches Programmiermodell bereit, das die Skalierbarkeit und Benutzerfreundlichkeit beim Entwickeln gleichzeitiger Anwendungen fördert. Die PPL baut auf den Planungs- und Ressourcenverwaltungskomponenten der Concurrency Runtime auf. Sie stuft die Abstraktionebene zwischen dem Anwendungscode und dem zugrunde liegenden Threadingmechanismus herauf, indem sie generische, typsichere Algorithmen und Container bereitstellt, die Daten parallel verarbeiten. Mit der PPL können Sie auch Anwendungen entwickeln, die durch die Bereitstellung von Alternativen zum freigegebenen Status skalieren.

Die PPL bietet die folgenden Funktionen:

  • Task-Parallelität: ein Mechanismus, der über dem Windows ThreadPool funktioniert, um mehrere Arbeitsaufgaben (Aufgaben) parallel auszuführen

  • Parallele Algorithmen: generische Algorithmen, die über der Parallelitätslaufzeit arbeiten, um auf Sammlungen von Daten parallel zu reagieren

  • Parallele Container und Objekte: generische Containertypen, die sicheren gleichzeitigen Zugriff auf ihre Elemente bieten

Beispiel

Die PPL stellt ein Programmiermodell bereit, das der C++-Standardbibliothek ähnelt. Im folgenden Beispiel werden zahlreiche PPL-Funktionen veranschaulicht. Darin werden mehrere Fibonacci-Zahlen seriell sowie parallel berechnet. Beide Berechnungen wirken auf ein std::array-Objekt . Das Beispiel gibt außerdem die Zeit, die zum Ausführen beider Berechnungen benötigt wird, in der Konsole aus.

Die serielle Version verwendet den C++-Standardbibliotheksalgorithmus "std::for_each ", um das Array zu durchlaufen und die Ergebnisse in einem std::vector-Objekt zu speichern. Die parallele Version führt dieselbe Aufgabe aus, verwendet jedoch die PPL-Parallelität ::p arallel_for_each Algorithmus und speichert die Ergebnisse in einem Parallelitätsobjekt::concurrent_vector objekt. Die concurrent_vector-Klasse aktiviert die einzelnen Schleifeniterationen, Elemente gleichzeitig hinzuzufügen, ohne dass erforderlich ist, den Schreibzugriff auf den Container zu synchronisieren.

Da parallel_for_each gleichzeitig ausführt, muss die parallele Version dieses Beispiels das concurrent_vector-Objekt so sortieren, dass die gleichen Ergebnisse wie bei der seriellen Version erzielt werden.

Beachten Sie, dass in dem Beispiel die Fibonacci-Zahlen mithilfe einer naiven Methode berechnet werden; diese Methode veranschaulicht jedoch, wie die Concurrency Runtime die Leistung bei langen Berechnungen verbessern kann.

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

Die folgende Beispielausgabe entspricht einem Ergebnis auf einem Computer mit vier Prozessoren.

serial time: 9250 ms
parallel time: 5726 ms

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

Jede Iteration der Schleife erfordert eine unterschiedliche Zeit zum Beenden. Die Leistung von parallel_for_each wird von der Operation bestimmt, die als Letztes beendet wird. Daher sollten Sie keine linearen Leistungsverbesserungen zwischen den seriellen und parallelen Versionen dieses Beispiels erwarten.

Titel Beschreibung
Task-Parallelität Beschreibt die Rolle von Aufgaben und Aufgabengruppen in der PPL.
Parallele Algorithmen Beschreibt die Verwendung parallele Algorithmen, wie z. B. parallel_for und parallel_for_each.
Parallele Container und Objekte Beschreibt die verschiedenen parallelen Container und die Objekte, die von der PPL bereitgestellt werden.
Abbruch in der PPL Erläutert, wie die von einem parallelen Algorithmus ausgeführte Verarbeitung abgebrochen wird.
Concurrency Runtime Beschreibt die Concurrency Runtime, die die parallele Programmierung vereinfacht, und stellt Links zu verwandten Themen bereit.