Empfohlene Vorgehensweisen in der Parallel Patterns Library

Dieses Dokument beschreibt die optimale, effektive Nutzung der Parallel Patterns Library (PPL). Die PPL bietet allgemeine Container, Objekte und Algorithmen zum Ausführen von differenziertem Parallelismus.

Weitere Informationen zur PPL finden Sie unter Parallel Patterns Library (PPL).

Abschnitte

Dieses Dokument enthält folgende Abschnitte:

Kleine Schleifenkörper nicht parallelisieren

Die Parallelisierung von relativ kleinen Schleifenkörpern kann dazu führen, dass der entsprechende Planungsaufwand die Vorteile der parallelen Verarbeitung zunichte machen. Betrachten Sie das folgende Beispiel, das jedes Elementpaar in zwei Arrays hinzufügt.

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

Die Arbeitsauslastung für jede Iteration der parallelen Schleife ist zu klein, um vom Aufwand für die parallele Verarbeitung zu profitieren. Sie können die Leistung dieser Schleife verbessern, indem Sie mehr Arbeit im Schleifenkörper oder durch das serielle Ausführen der Schleife erledigen.

[Nach oben]

Express-Parallelität auf höchster Ebene

Wenn Sie Code nur auf niedriger Ebene parallelisieren, können Sie ein Fork-Join-Konstrukt einführen, das nicht mit der steigenden Anzahl der Prozessoren skaliert. Ein Verzweigungskonstrukt ist ein Konstrukt, bei dem ein Vorgang seine Arbeit in kleinere parallele Teilvorgänge aufteilt und auf den Abschluss dieser Teilvorgänge wartet. Jede Unteraufgabe kann sich selbst rekursiv auf weitere Unteraufgaben aufteilen.

Obwohl das Fork-Join-Modell zur Lösung verschiedener Probleme hilfreich sein kann, gibt es Situationen, in denen der Synchronisierungsaufwand die Skalierbarkeit verringern kann. Betrachten Sie beispielsweise den folgenden seriellen Code, der Bilddaten verarbeitet.

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

Da jede Schleifeniteration unabhängig ist, können Sie einen Großteil der Arbeit parallelisieren, wie im folgenden Beispiel gezeigt wird. In diesem Beispiel wird die Parallelität::p arallel_for-Algorithmus verwendet , um die äußere Schleife zu parallelisieren.

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

Das folgende Beispiel veranschaulicht ein Fork-Join-Konstrukt durch Aufrufen der ProcessImage-Funktion in einer Schleife. Jeder Aufruf von ProcessImage wird erst zurückgegeben, wenn jede Unteraufgabe abgeschlossen ist.

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

Wenn jede Iteration der parallelen Schleife fast keine Arbeit ausführt oder die Arbeit, die von der parallelen Schleife ausgeführt wird, unausgeglichenen ist, d. h. einige Schleifeniterationen dauern länger als andere, kann der Planungsaufwand, der erforderlich ist, um Arbeit häufig aufzuteilen und zu verknüpfen, die Vorteile für die parallele Ausführung überwiegen. Dieser Aufwand nimmt mit der Anzahl der Prozessoren zu.

Um den Planungsaufwand in diesem Beispiel zu reduzieren, können Sie äußere Schleifen parallelisieren, bevor Sie innere Schleifen parallelisieren, oder Sie verwenden ein anderes paralleles Konstrukt, z. B. Pipelines. Im folgenden Beispiel wird die ProcessImages Funktion so geändert, dass die Parallelität::p arallel_for_each Algorithmus verwendet wird, um die äußere Schleife zu parallelisieren.

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

Ein ähnliches Beispiel, das eine Pipeline zum parallelen Ausführen der Bildverarbeitung verwendet, finden Sie unter Walkthrough: Creating an Image Processing Network.

[Nach oben]

Verwenden Sie parallel_invoke, um Probleme bei der Divid-and-Conquer-Lösung zu lösen.

Ein Dividierungsproblem ist eine Form des Verzweigungskonstrukts, das Rekursion verwendet, um einen Vorgang in Teilvorgänge aufzuteilen. Zusätzlich zu den Parallelitätsklassen::task_group und Parallelität::structured_task_group Klassen können Sie auch den Concurrency::p arallel_invoke-Algorithmus verwenden, um Probleme bei der Aufteilung und Erzwingung zu lösen. Die parallel_invoke-Algorithmus verfügt über eine kompaktere Syntax als Aufgabengruppenobjekte und ist nützlich, wenn Sie eine feste Anzahl paralleler Aufgaben haben.

Das folgende Beispiel veranschaulicht die Verwendung des parallel_invoke-Algorithmus zum Implementieren des bitonischen Sortieralgorithmus.

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

Mit dem parallel_invoke-Algorithmus wird der Aufwand reduziert, indem die letzte einer Reihe von Aufgaben im aufrufenden Kontext ausgeführt wird.

Die vollständige Version dieses Beispiels finden Sie unter How to: Use parallel_invoke to Write a Parallel Sort Routine. Weitere Informationen zum parallel_invoke Algorithmus finden Sie unter Parallel-Algorithmen.

[Nach oben]

Verwenden der Abbruch- oder Ausnahmebehandlung zum Unterbrechen einer parallelen Schleife

Die PPL bietet zwei Möglichkeiten, um parallele Arbeitsvorgänge abzubrechen, die von einer Aufgabengruppe oder einem parallelen Algorithmus ausgeführt werden. Eine Möglichkeit besteht darin, den Abbruchmechanismus zu verwenden, der von der Parallelität::task_group und parallelen Klassen::structured_task_group bereitgestellt wird. Eine andere Möglichkeit ist das Auslösen einer Ausnahme im Text einer Arbeitsfunktion einer Aufgabe. Der Abbruchmechanismus ist effizienter als die Ausnahmebehandlung beim Abbrechen einer parallelen Arbeitsstruktur. Eine parallele Arbeitsstruktur ist eine Gruppe verwandter Aufgabengruppen, in denen einige Aufgabengruppen andere Aufgabengruppen enthalten. Der Abbruchmechanismus bricht eine Aufgabengruppe und ihre untergeordneten Aufgabengruppen von oben nach unten ab. Bei der Ausnahmebehandlung wird dagegen die umgekehrte Reihenfolge verwendet (Bottom-Up-Ansatz), sodass jede untergeordnete Aufgabengruppe einzeln abgebrochen werden muss.

Wenn Sie direkt mit einem Aufgabengruppenobjekt arbeiten, verwenden Sie die Parallelität::task_group::cancel oder parallelcurrency::structured_task_group::cancel-Methoden , um die Arbeit abzubrechen, die zu dieser Aufgabengruppe gehört. Zum Abbrechen eines parallelen Algorithmus, z. B. parallel_for, erstellen Sie eine übergeordnete Aufgabengruppe brechen diese Aufgabengruppe ab. Betrachten Sie beispielsweise die folgende Funktion parallel_find_any, die parallel nach einem Wert in einem Array sucht.

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

Da parallele Algorithmen Aufgabengruppen verwenden, wenn eine der parallelen Iterationen die übergeordnete Aufgabengruppe abbricht, wird die gesamte Aufgabe abgebrochen. Die vollständige Version dieses Beispiels finden Sie unter How to: Use Cancellation to Break from a Parallel Loop.

Obwohl die Ausnahmebehandlung eine weniger effiziente Möglichkeit zum Abbrechen paralleler Aufgaben als der Abbruch ist, gibt es Fälle, in denen Ausnahmebehandlung sinnvoll ist. Zum Beispiel führt die folgende Methode, for_all, rekursiv eine Arbeitsfunktion auf jedem Knoten einer tree-Struktur aus. In diesem Beispiel ist das _children Datenelement ein std::list , das Objekte enthält 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);
}

Der Aufrufer der tree::for_all-Methode kann eine Ausnahme auslösen, wenn es nicht erforderlich ist, dass die Arbeitsfunktion für jedes Element der Struktur aufgerufen wird. Das folgende Beispiel zeigt die search_for_value-Funktion, die nach einem Wert im bereitgestellten tree-Objekt sucht. Die search_for_value-Funktion verwendet eine Arbeitsfunktion, die eine Ausnahme auslöst, wenn das aktuelle Element der Struktur mit dem bereitgestellten Wert übereinstimmt. Die search_for_value-Funktion verwendet einen try-catch-Block, um die Ausnahme zu erfassen und das Ergebnis in der Konsole auszugeben.

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

Die vollständige Version dieses Beispiels finden Sie unter How to: Use Exception Handling to Break from a Parallel Loop.

Allgemeine Informationen zu den von der PPL bereitgestellten Abbruch- und Ausnahmebehandlungsmechanismen finden Sie unter "Abbruch" in der PPL und der Ausnahmebehandlung.

[Nach oben]

Verstehen, wie sich die Abbruch- und Ausnahmebehandlung auf die Objektvernichtung auswirkt

Eine abgebrochene Aufgabe in einer Struktur paralleler Arbeitsaufgaben kann dazu führen, dass untergeordnete Aufgaben nicht ausgeführt werden. Dies kann Probleme verursachen, wenn eine der untergeordneten Aufgaben einen Vorgang ausführen soll, der für die Anwendung von Bedeutung ist, beispielsweise das Freigeben einer Ressource. Darüber hinaus kann ein Aufgabenabbruch eine Ausnahme über einen Objektdestruktor weitergeben und ein nicht definiertes Verhalten in der Anwendung verursachen.

Im folgenden Beispiel beschreibt die Resource-Klasse eine Ressource und die Container-Klasse einen Container, der Ressourcen enthält. Im ihrem Destruktor ruft die Container-Klasse die cleanup-Methode für zwei ihrer Resource-Member parallel auf und ruft dann die cleanup-Methode für den dritten Resource-Member auf.

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

Obwohl dieses Muster selbst keine Probleme hat, sollten Sie den folgenden Code verwenden, der zwei Aufgaben parallel ausführt. Die erste Aufgabe erstellt ein Container-Objekt, und die zweite Aufgabe bricht die gesamte Aufgabe ab. Zur Veranschaulichung verwendet das Beispiel zwei Parallelitätsobjekte::event , um sicherzustellen, dass der Abbruch nach dem Erstellen des Container Objekts auftritt und dass das Container Objekt nach dem Abbruchvorgang zerstört wird.

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

Dieses Beispiel erzeugt die folgende Ausgabe:

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

Dieses Codebeispiel enthält die folgenden Probleme, die dazu führen, dass es sich möglicherweise anders als erwartet verhält:

  • Der Abbruch der übergeordneten Aufgabe bewirkt, dass die untergeordnete Aufgabe, der Aufruf von concurrency::p arallel_invoke, ebenfalls abgebrochen wird. Aus diesem Grund werden diese beiden Ressourcen nicht freigegeben.

  • Der Abbruch der übergeordneten Aufgabe bewirkt, dass die untergeordnete Aufgabe eine interne Ausnahme auslöst. Da der Container-Destruktor diese Ausnahme nicht behandelt, wird die Ausnahme aufwärts weitergegeben, und die dritte Ressource wird nicht freigegeben.

  • Die Ausnahme, die von der untergeordneten Aufgabe ausgelöst wird, wird über den Container-Destruktor weitergegeben. Durch das Auslösen von einem Destruktor wird die Anwendung in einen nicht definierten Zustand versetzt.

Es wird empfohlen, keine wichtigen Vorgänge, z. B. das Freigeben von Ressourcen, in den Aufgaben auszuführen, sofern Sie nicht garantieren können, dass diese Aufgaben nicht abgebrochen werden. Außerdem wird empfohlen, keine Laufzeitfunktionen zu verwenden, die im Destruktor der Typen ausgelöst werden können.

[Nach oben]

Wiederholtes Blockieren in einer parallelen Schleife nicht blockieren

Eine parallele Schleife wie "concurrency::p arallel_for " oder "concurrency::p arallel_for_each ", die von Blockierungsvorgängen dominiert wird, kann dazu führen, dass die Laufzeit viele Threads über einen kurzen Zeitraum erstellt.

Die Concurrency Runtime führt zusätzliche Arbeiten aus, wenn eine Aufgabe beendet oder kooperativ blockiert oder zurückgehalten wird. Wenn eine parallelen Schleifeniteration blockiert, kann die Laufzeit eine andere Iteration starten. Wenn keine Leerlaufthreads verfügbar sind, erstellt die Laufzeit einen neuen Thread.

Wenn der Text einer parallelen Schleife gelegentlich blockt, hilft dieser Mechanismus beim Maximieren des Gesamtaufgabendurchsatzes. Wenn viele Iterationen blockieren, kann die Laufzeit viele Threads erstellen, um die zusätzlichen Arbeitsvorgänge auszuführen. Dies kann zu Arbeitsspeichermangel oder unzureichender Nutzung von Hardwareressourcen führen.

Betrachten Sie das folgende Beispiel, das die Parallelität::send-Funktion in jeder Iteration einer parallel_for Schleife aufruft. Da send kooperativ blockiert, erstellt die Laufzeit einen neuen Thread, um zusätzliche Arbeit bei jedem Aufruf von send auszuführen.

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

Wir empfehlen, den Code neu zu gestalten, um dieses Muster zu vermeiden. In diesem Beispiel können Sie die Erstellung zusätzlicher Threads vermeiden, indem Sie send in einen seriellen for-Schleife aufrufen.

[Nach oben]

Ausführen von Blockierungsvorgängen beim Abbrechen der parallelen Arbeit nicht

Führen Sie nach Möglichkeit keine Blockierungsvorgänge durch, bevor Sie die Parallelität::task_group::cancel oder concurrency::structured_task_group::cancel-Methode aufrufen, um die parallele Arbeit abzubrechen.

Wenn eine Aufgabe einen kooperativen Blockierungsvorgang ausführt, kann die Laufzeit andere Aufgaben ausführen, während die erste Aufgabe auf Daten wartet. Die Laufzeit plant die wartende Aufgabe neu, wenn die Blockierung aufgehoben wird. Die Laufzeit plant in der Regel Aufgaben neu, die vor kurzem entsperrt wurden, bevor sie Aufgaben, die vor längerer Zeit entsperrt wurden, neu plant. Aus diesem Grund könnte die Laufzeit unnötige Arbeit während des blockierenden Vorgangs planen, was zu Leistungseinbußen führt. Wenn Sie also einen Blockierungsvorgang ausführen, bevor Sie parallele Arbeitsvorgänge abbrechen, kann der blockierende Vorgang den Aufruf von cancel verzögern. Dies führt dazu, dass andere Aufgaben unnötige Arbeit ausführen.

Das folgende Beispiel definiert die parallel_find_answer-Funktion, die ein Element des bereitgestellten Arrays sucht, das der angegebenen Prädikatfunktion entspricht. Wenn die Prädikatfunktion zurückgegeben wird true, erstellt die parallele Arbeitsfunktion ein Answer Objekt und bricht den Gesamtvorgang ab.

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

Der new-Operator führt eine Heapzuordnung aus, die blockieren könnte. Die Laufzeit führt nur andere Aufgaben aus, wenn die Aufgabe einen kooperativen Blockierungsaufruf ausführt, z. B. einen Aufruf zur Parallelität::critical_section::lock.

Das folgende Beispiel zeigt, wie Sie unnötige Arbeiten verhindern und damit die Leistung verbessern. In diesem Beispiel wird die Aufgabengruppe abgebrochen, bevor sie den Speicher für das Answer-Objekt zuweist.

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

[Nach oben]

In freigegebene Daten in einer parallelen Schleife nicht schreiben

Die Parallelitätslaufzeit stellt mehrere Datenstrukturen bereit, z. B. Parallelität::critical_section, die gleichzeitigen Zugriff auf freigegebene Daten synchronisieren. Diese Datenstrukturen sind in vielen Fällen hilfreich, wenn z. B. mehrere Aufgaben nur selten gemeinsamen Zugriff auf eine Ressource erfordern.

Betrachten Sie das folgende Beispiel, das die Parallelität::p arallel_for_each Algorithmus und ein critical_section Objekt verwendet, um die Anzahl der Primzahlen in einem std::array-Objekt zu berechnen. Dieses Beispiel skaliert nicht, da jeder Thread warten muss, um auf die freigegebene Variable prime_sum zuzugreifen.

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

Dieses Beispiel kann auch zu Leistungseinbußen führen, da der häufige Sperrvorgang die Schleife effektiv serialisiert. Wenn darüber hinaus ein Concurrency Runtime-Objekt einen Blockierungsvorgang ausführt, erstellt der Planer möglicherweise einen zusätzlichen Thread, um andere Aufgaben auszuführen, während der erste Thread auf Daten wartet. Wenn die Laufzeit viele Threads erstellt, da viele Aufgaben auf freigegebene Daten warten, kann die Anwendungsleistung abnehmen oder die Anwendung in einen Ressourcenmangelzustand versetzt werden.

Die PPL definiert die parallele Klasse::kombinationsfähige Klasse, die Ihnen hilft, den gemeinsam genutzten Zustand zu beseitigen, indem Sie zugriff auf freigegebene Ressourcen auf sperrfreie Weise bereitstellen. Die combinable-Klasse stellt lokalen Threadspeicher bereit, mit dem Sie differenzierte Berechnungen ausführen und dann diese Berechnungen in einem Endergebnis zusammenführen können. Stellen Sie sich ein combinable-Objekt wie eine Reduktionsvariable vor.

Im folgenden Beispiel wird das vorherige mithilfe eines combinable-Objekts anstelle eines critical_section-Objekts zum Berechnen der Summe geändert. Dieses Beispiel skaliert, da jeder Thread eine eigene lokale Kopie der Summe enthält. In diesem Beispiel wird die Parallelität::combinable::combine-Methode verwendet, um die lokalen Berechnungen mit dem Endergebnis zusammenzuführen.

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

Die vollständige Version dieses Beispiels finden Sie unter How to: Use combinable to Improve Performance. Weitere Informationen zur combinable Klasse finden Sie unter Parallel Containers and Objects.

[Nach oben]

Vermeiden Sie nach Möglichkeit falsche Freigaben.

False sharing occurs when multiple concurrent tasks that are running on separate processor write to variables that are located on the same cache line. Wenn eine Aufgabe in eine der Variablen schreibt, wird die Cachezeile für beide Variablen ungültig. Jeder Prozessor muss die Cachezeile jedes Mal neu laden, wenn die Cachezeile ungültig ist. Daher kann False Sharing zu Leistungseinbußen in der Anwendung führen.

Das folgende grundlegende Beispiel zwei gleichzeitige Aufgaben, die jeweils eine gemeinsame Zählervariable erhöhen.

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

Um die gemeinsame Nutzung von Daten zwischen den beiden Aufgaben zu vermeiden, können Sie das Beispiel so ändern, dass zwei Zählervariablen verwendet werden. In diesem Beispiel wird der endgültige Zählerwert berechnet, nachdem die Aufgaben beendet wurden. Dieses Beispiel veranschaulicht jedoch False Sharing, da sich die Variablen count1 und count2 wahrscheinlich in derselben Cachezeile befinden.

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;

Eine Möglichkeit zur Eliminierung von False Sharing besteht darin, sicherzustellen, dass sich die Zählervariablen in unterschiedlichen Cachezeilen befinden. Das folgende Beispiel richtet die Variablen count1 und count2 auf 64-Byte-Grenzen aus.

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

In diesem Beispiel wird davon ausgegangen, dass die Größe des Arbeitsspeichercaches höchstens 64 Byte beträgt.

Es wird empfohlen, die parallele Klasse::kombinationsfähige Klasse zu verwenden, wenn Sie Daten zwischen Vorgängen freigeben müssen. Die combinable-Klasse erstellt lokale Threadvariablen derart, dass False Sharing weniger wahrscheinlich ist. Weitere Informationen zur combinable Klasse finden Sie unter Parallel Containers and Objects.

[Nach oben]

Stellen Sie sicher, dass Variablen während der gesamten Lebensdauer eines Vorgangs gültig sind.

Wenn Sie einer Aufgabengruppe oder einem parallelen Algorithmus einen Lambdaausdruck bereitstellen, gibt die Erfassungsklausel an, ob der Text des Lambdaausdrucks auf Variablen im einschließenden Bereich als Wert oder als Verweis zugreift. Wenn Sie Variablen als Verweis an einen Lambdaausdruck übergeben, müssen Sie sicherstellen, dass die Lebensdauer dieser Variablen bis zum Beenden der Aufgabe erhalten bleibt.

Betrachten Sie das folgende Beispiel, in dem die object-Klasse und die perform_action-Funktion definiert werden. Die perform_action-Funktion erstellt eine object-Variable und führt eine Aktion für diese Variable asynchron durch. Da die Aufgabe nicht unbedingt vor der Rückgabe der perform_action-Funktion beendet wird, stürzt das Programm ab oder zeigt nicht definiertes Verhalten, wenn die object-Variable beim Ausführen der Aufgabe gelöscht wird.

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

Je nach den Anforderungen Ihrer Anwendung können Sie eine der folgenden Methoden verwenden, um zu garantieren, dass Variablen während der Lebensdauer der einzelnen Aufgaben gültig bleiben.

Das folgende Beispiel übergibt die object-Variable als Wert an die Aufgabe. Aus diesem Grund funktioniert die Aufgabe mit der eigenen Kopie der Variablen.

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

Da die object-Variable als Wert übergeben wird, werden alle Zustandsänderungen, die für diese Variablen auftreten, nicht in der ursprünglichen Kopie angezeigt.

Im folgenden Beispiel wird die Parallelität::task_group::wait-Methode verwendet, um sicherzustellen, dass der Vorgang abgeschlossen ist, bevor die perform_action Funktion zurückgegeben wird.

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

Da die Aufgabe jetzt abgeschlossen ist, bevor die Funktion beendet wird, verhält sich die perform_action-Funktion nicht mehr asynchron.

Das folgende Beispiel ändert die perform_action-Funktion für die Übergabe eines Verweises an die object-Variable. Der Aufrufer muss sicherstellen, dass die Lebensdauer der object-Variable gültig ist, bis die Aufgabe abgeschlossen ist.

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

Sie können auch einen Zeiger zur Steuerung der Lebensdauer eines Objekts verwenden, das Sie an eine Aufgabengruppe oder einen parallelen Algorithmus übergeben.

Weitere Informationen zu Lambdaausdrücken finden Sie unter Lambda Expressions (Lambdaausdrücke).

[Nach oben]

Siehe auch

Bewährte Methoden im Zusammenhang mit der Concurrency Runtime
Parallel Patterns Library (PPL)
Parallele Container und Objekte
Parallele Algorithmen
Abbruch in der PPL
Ausnahmebehandlung
Exemplarische Vorgehensweise: Erstellen eines Bildverarbeitungsnetzwerks
Vorgehensweise: Verwenden von parallel_invoke zum Schreiben einer Runtime für paralleles Sortieren
Vorgehensweise: Verwenden eines Abbruchs zum Verlassen einer Parallel-Schleife
Vorgehensweise: Verbessern der Leistung mithilfe von combinable
Bewährte Methoden in der asynchronen Agents Library
Allgemein bewährte Methoden in der Concurrency Runtime