Delen via


Aanbevolen procedures in de bibliotheek parallelle patronen

In dit document wordt beschreven hoe u effectief gebruik kunt maken van de PPL (Parallel Patterns Library). De PPL biedt containers, objecten en algoritmen voor algemeen gebruik voor het uitvoeren van fijnmazige parallelle uitvoering.

Zie De PPL (Parallel Patterns Library) voor meer informatie over de PPL.

Afdelingen

Dit document bevat de volgende secties:

Parallelliseer kleine luslichamen niet

De parallelle uitvoering van relatief kleine luslichamen kan ertoe leiden dat de bijbehorende planningsoverhead zwaarder weegt dan de voordelen van parallelle verwerking. Bekijk het volgende voorbeeld, waarmee elk paar elementen in twee matrices wordt toegevoegd.

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

De werklast voor elke parallelle lusiteratie is te klein om te profiteren van de overhead bij parallelle verwerking. U kunt de prestatie van deze lus verbeteren door meer werk uit te voeren in het luslichaam of door de lus achtereenvolgens uit te voeren.

[Boven]

Express parallelisme op het hoogste mogelijke niveau

Wanneer u code alleen op laag niveau parallelliseert, kunt u een fork-join-constructie introduceren die niet wordt geschaald naarmate het aantal processors toeneemt. Een fork-join-construct is een constructie waarbij een taak het werk verdeelt in kleinere parallelle subtaken en wacht tot deze subtaken voltooid zijn. Elke subtaak kan zich recursief verdelen in extra subtaken.

Hoewel het fork-joinmodel nuttig kan zijn voor het oplossen van verschillende problemen, zijn er situaties waarin de overhead van de synchronisatie de schaalbaarheid kan verminderen. Denk bijvoorbeeld aan de volgende seriële code waarmee afbeeldingsgegevens worden verwerkt.

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

Omdat elke herhaling van de lus onafhankelijk is, kunt u veel van het werk parallel uitvoeren, zoals geïllustreerd in het volgende voorbeeld. In dit voorbeeld wordt het algoritme concurrency::parallel_for gebruikt om de buitenste lus te parallelliseren.

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

In het volgende voorbeeld ziet u een fork-join-constructie door de ProcessImage functie in een lus aan te roepen. Elke aanroep naar ProcessImage wordt pas geretourneerd als elke subtaak is voltooid.

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

Als elke iteratie van de parallelle lus bijna geen werk uitvoert of als het werk dat door de parallelle lus wordt uitgevoerd, onevenwichtig is, dat wil gezegd dat sommige lusiteraties langer duren dan andere, kan de planningsoverhead die nodig is om vaak te forken en werk samenvoegen, opwegen tegen het voordeel van parallelle uitvoering. Deze overhead neemt toe naarmate het aantal processors toeneemt.

Als u de overhead van de planning in dit voorbeeld wilt verminderen, kunt u buitenste lussen parallelliseren voordat u binnenlussen parallelliseert of een andere parallelle constructie zoals pipelining gebruikt. In het volgende voorbeeld wordt de functie ProcessImages gewijzigd om de concurrency::parallel_for_each algoritme te gebruiken om de buitenste lus te parallelliseren.

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

Zie Walkthrough: Een Image-Processing-netwerk maken voor een vergelijkbaar voorbeeld waarin een pijplijn wordt gebruikt om parallelle afbeeldingsverwerking uit te voeren.

[Boven]

Gebruik parallel_invoke om problemen met delen en veroveren op te lossen

Een probleem met delen en veroveren is een vorm van de fork-join-constructie die gebruikmaakt van recursie om een taak in subtaken te splitsen. Naast de concurrency::task_group en concurrency::structured_task_group kunt u ook het concurrency::parallel_invoke-algoritme gebruiken om deel-en-heersproblemen op te lossen. Het parallel_invoke algoritme heeft een beknoptere syntaxis dan taakgroepobjecten en is handig wanneer u een vast aantal parallelle taken hebt.

In het volgende voorbeeld ziet u het gebruik van het parallel_invoke algoritme voor het implementeren van het bitonische sorteeralgoritmen.

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

Om de overhead te verminderen, voert het parallel_invoke algoritme de laatste van de reeks taken uit voor de aanroepende context.

Zie Voor de volledige versie van dit voorbeeld : Gebruik parallel_invoke om een parallelle sorteerroutine te schrijven. Zie parallel_invoke voor meer informatie over het algoritme.

[Boven]

Annulerings- of uitzonderingsafhandeling gebruiken om een parallelle lus te verbreken

De PPL biedt twee manieren om het parallelle werk te annuleren dat wordt uitgevoerd door een taakgroep of parallel algoritme. Een manier is om het annuleringsmechanisme te gebruiken dat wordt geleverd door de gelijktijdigheid::task_group en gelijktijdigheid::structured_task_group klassen. U kunt ook een uitzondering genereren in de hoofdtekst van een taakwerkfunctie. Het annuleringsmechanisme is efficiënter dan het verwerken van uitzonderingen bij het annuleren van een structuur van parallelle werkzaamheden. Een parallelle werkstructuur is een groep gerelateerde taakgroepen waarin sommige taakgroepen andere taakgroepen bevatten. Met het annuleringsmechanisme worden een taakgroep en de onderliggende taakgroepen op een top-down manier geannuleerd. Omgekeerd werkt de afhandeling van uitzonderingen van onder naar boven en moet elke onderliggende taakgroep onafhankelijk worden geannuleerd terwijl de uitzondering naar boven wordt doorgegeven.

Wanneer u rechtstreeks met een taakgroepobject werkt, gebruikt u de gelijktijdigheid::task_group::annuleren of gelijktijdigheid::structured_task_group::annuleren methoden om het werk dat deel uitmaakt van die taakgroep te annuleren. Als u een parallel algoritme wilt annuleren, maakt u bijvoorbeeld een bovenliggende taakgroep parallel_for en annuleert u die taakgroep. Denk bijvoorbeeld aan de volgende functie, parallel_find_anydie zoekt naar een waarde in een matrix parallel.

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

Omdat parallelle algoritmen taakgroepen gebruiken, wordt de algehele taak geannuleerd wanneer een van de parallelle iteraties de bovenliggende taakgroep annuleert. Zie voor de volledige versie van dit voorbeeld Hoe: Annulering gebruiken om een parallelle lus te onderbreken.

Hoewel het verwerken van uitzonderingen een minder efficiënte manier is om parallelle werkzaamheden te annuleren dan het annuleringsmechanisme, zijn er gevallen waarin de afhandeling van uitzonderingen geschikt is. De volgende methode voert for_allbijvoorbeeld recursief een werkfunctie uit op elk knooppunt van een tree structuur. In dit voorbeeld is het _children gegevenslid een std::list die objecten bevat 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);
}

De aanroeper van de tree::for_all methode kan een uitzondering genereren als de werkfunctie niet op elk element van de structuur moet worden aangeroepen. In het volgende voorbeeld ziet u de search_for_value functie, waarmee wordt gezocht naar een waarde in het opgegeven tree object. De search_for_value functie maakt gebruik van een werkfunctie die een uitzondering genereert wanneer het huidige element van de structuur overeenkomt met de opgegeven waarde. De search_for_value functie gebruikt een try-catch blok om de uitzondering vast te leggen en het resultaat af te drukken naar de console.

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

Zie voor de volledige versie van dit voorbeeld Hoe gebruikt u uitzonderingsafhandeling om een parallelle lus te onderbreken.

Zie Annulering in de PPL en De verwerking van uitzonderingen voor meer algemene informatie over de annulerings- en uitzonderingsafhandelingsmechanismen die door de PPL worden verstrekt.

[Boven]

Begrijpen hoe annulering en afhandeling van uitzonderingen invloed hebben op objectvernietiging

In een structuur van parallelle werkzaamheden voorkomt een taak die wordt geannuleerd dat onderliggende taken worden uitgevoerd. Dit kan problemen veroorzaken als een van de onderliggende taken een bewerking uitvoert die belangrijk is voor uw toepassing, zoals het vrijmaken van een resource. Daarnaast kan het annuleren van taken ertoe leiden dat een uitzondering wordt doorgegeven via een objectdestructor en niet-gedefinieerd gedrag in uw toepassing veroorzaakt.

In het volgende voorbeeld beschrijft de Resource klasse een resource en de Container klasse beschrijft een container die resources bevat. In de destructor roept de Container klasse de cleanup methode aan op twee van de Resource leden parallel en roept de methode vervolgens aan cleanup op het derde Resource lid.

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

Hoewel dit patroon zelf geen problemen heeft, kunt u rekening houden met de volgende code waarmee twee taken parallel worden uitgevoerd. De eerste taak maakt een Container object en de tweede taak annuleert de algehele taak. Ter illustratie maakt het voorbeeld gebruik van twee concurrency::event objecten om ervoor te zorgen dat de annulering plaatsvindt nadat het Container object is gemaakt en dat het Container object wordt vernietigd nadat de annulering heeft plaatsgevonden.

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

In dit voorbeeld wordt de volgende uitvoer gegenereerd:

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

Dit codevoorbeeld bevat de volgende problemen die ertoe kunnen leiden dat het zich anders gedraagt dan verwacht:

  • De annulering van de oudertaak zorgt ervoor dat de kindtaak, de aanroep voor gelijktijdigheid::parallel_invoke, ook wordt geannuleerd. Daarom worden deze twee hulpbronnen niet vrijgemaakt.

  • De annulering van de bovenliggende taak zorgt ervoor dat de onderliggende taak een interne uitzondering genereert. Omdat de Container destructor deze uitzondering niet verwerkt, wordt de uitzondering naar boven doorgegeven en wordt de derde resource niet vrijgemaakt.

  • De uitzondering die door de onderliggende taak wordt opgeworpen, wordt doorgegeven via de Container destructor. Als u een destructor gooit, wordt de toepassing in een niet-gedefinieerde status gebracht.

U wordt aangeraden geen kritieke bewerkingen uit te voeren, zoals het vrijmaken van resources, in taken, tenzij u kunt garanderen dat deze taken niet worden geannuleerd. U wordt ook aangeraden geen runtime-functionaliteit te gebruiken die destructor van uw typen kan veroorzaken.

[Boven]

Niet herhaaldelijk blokkeren binnen een parallel loop

Een parallelle lus, zoals concurrency::parallel_for of concurrency::parallel_for_each, die door blokkadebewerkingen gedomineerd wordt, kan ertoe leiden dat de runtime gedurende een korte tijd veel threads maakt.

De Concurrentieruntime voert extra werk uit wanneer een taak wordt voltooid, coöperatief wordt geblokkeerd of inlevert. Wanneer één parallelle lus-iteratie wordt geblokkeerd, kan de runtime een andere iteratie starten. Wanneer er geen inactieve threads beschikbaar zijn, maakt de runtime een nieuwe thread.

Wanneer het lichaam van een parallelle lus af en toe blokkeert, helpt dit mechanisme de totale taakdoorvoer te maximaliseren. Wanneer veel iteraties echter worden geblokkeerd, kan de runtime veel threads maken om het extra werk uit te voeren. Dit kan leiden tot slechte geheugenomstandigheden of slecht gebruik van hardwareresources.

Bekijk het volgende voorbeeld waarin de concurrency::send functie wordt aangeroepen in elke iteratie van een parallel_for lus. Omdat send blokken coöperatief worden geblokkeerd, maakt de runtime telkens wanneer send wordt aangeroepen een nieuwe thread om extra werk uit te voeren.

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

U wordt aangeraden uw code te herstructureren om dit patroon te voorkomen. In dit voorbeeld kunt u het maken van extra threads voorkomen door send in een seriële for lus aan te roepen.

[Boven]

Geen blokkeringsbewerkingen uitvoeren wanneer u parallel werk annuleert

Voer indien mogelijk geen blokkeringsbewerkingen uit voordat u de methoden concurrency::task_group::cancel of concurrency::structured_task_group::cancel aanroept om parallel werk te annuleren.

Wanneer een taak een coöperatieve blokkeringsbewerking uitvoert, kan de runtime ander werk uitvoeren terwijl de eerste taak wacht op gegevens. De runtime herschikt de wachttaak wanneer deze wordt gedeblokkeerd. De runtime herschikt doorgaans taken die recenter zijn gedeblokkeerd voordat taken die minder recent zijn gedeblokkeerd, opnieuw worden gepland. Daarom kan de runtime onnodig werk plannen tijdens de blokkeringsbewerking, wat leidt tot verminderde prestaties. Wanneer u een blokkeringsbewerking uitvoert voordat u parallelle werkzaamheden annuleert, kan de blokkeringsbewerking de aanroep cancelvertragen. Dit zorgt ervoor dat andere taken onnodig werk uitvoeren.

Bekijk het volgende voorbeeld dat de parallel_find_answer functie definieert, waarmee wordt gezocht naar een element van de opgegeven matrix die voldoet aan de opgegeven predicaatfunctie. Wanneer de predicaatfunctie true retourneert, creëert de parallelwerkfunctie een Answer object en annuleert de algehele taak.

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

De new operator voert een heap-toewijzing uit, wat mogelijk voor een blokkering kan zorgen. De runtime voert alleen ander werk uit wanneer de taak een coöperatieve blokkeringsoproep uitvoert, zoals een aanroep naar gelijktijdigheid::critical_section::lock.

In het volgende voorbeeld ziet u hoe u onnodig werk kunt voorkomen en zo de prestaties kunt verbeteren. In dit voorbeeld wordt de taakgroep geannuleerd voordat de opslag voor het Answer object wordt toegewezen.

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

[Boven]

Niet schrijven naar gedeelde gegevens in een parallelle loop

De Gelijktijdigheidsruntime biedt verschillende gegevensstructuren, bijvoorbeeld gelijktijdigheid::critical_section, waarmee gelijktijdige toegang tot gedeelde gegevens wordt gesynchroniseerd. Deze gegevensstructuren zijn in veel gevallen handig, bijvoorbeeld wanneer meerdere taken zelden gedeelde toegang tot een resource vereisen.

Bekijk het volgende voorbeeld waarin gebruik wordt gemaakt van de gelijktijdigheid::p arallel_for_each-algoritme en een critical_section object om het aantal priemgetallen in een std::array-object te berekenen. Dit voorbeeld wordt niet geschaald omdat elke thread moet wachten op toegang tot de gedeelde variabele 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();
});

Dit voorbeeld kan ook leiden tot slechte prestaties omdat de frequente vergrendelingsbewerking de lus effectief serialiseert. Bovendien kan de scheduler, wanneer een Concurrence Runtime-object een blokkeringsbewerking uitvoert, een extra thread maken om ander werk uit te voeren terwijl de eerste thread wacht op data. Als de runtime veel threads maakt omdat veel taken wachten op gedeelde gegevens, kan de toepassing slecht presteren of een status met een lage resource invoeren.

De PPL definieert de gelijktijdigheid::combinatieklasse , waarmee u de gedeelde status kunt elimineren door op een vergrendelingsvrije manier toegang te verlenen tot gedeelde resources. De combinable klasse biedt thread-lokale opslag waarmee u gedetailleerde berekeningen kunt uitvoeren en deze berekeningen vervolgens kunt samenvoegen tot een eindresultaat. U kunt een combinable object beschouwen als een reductievariabele.

In het volgende voorbeeld wordt de vorige gewijzigd met behulp van een combinable object in plaats van een critical_section object om de som te berekenen. Dit voorbeeld wordt geschaald omdat elke thread een eigen lokale kopie van de som bevat. In dit voorbeeld wordt de parallelisme::combinable::combine-methode gebruikt om de lokale berekeningen samen te voegen in het uiteindelijke resultaat.

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

Zie Hoe: Gebruik combinable om de prestaties te verbeteren voor de volledige versie van dit voorbeeld. Zie combinable voor meer informatie over de klasse.

[Boven]

Indien mogelijk vermijdt u onwaar delen

False sharing treedt op wanneer meerdere gelijktijdige taken op afzonderlijke processors schrijven naar variabelen die zich op dezelfde cacheregel bevinden. Wanneer één taak naar een van de variabelen schrijft, wordt de cacheregel voor beide variabelen ongeldig gemaakt. Elke processor moet de cacheregel telkens opnieuw laden wanneer de cacheregel ongeldig is. Daarom kan onwaar delen leiden tot verminderde prestaties in uw toepassing.

In het volgende basisvoorbeeld ziet u twee gelijktijdige taken die elk een gedeelde tellervariabele verhogen.

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

Als u het delen van gegevens tussen de twee taken wilt elimineren, kunt u het voorbeeld wijzigen om twee tellervariabelen te gebruiken. In dit voorbeeld wordt de uiteindelijke tellerwaarde berekend nadat de taken zijn voltooid. Dit voorbeeld illustreert echter false sharing, omdat de variabelen count1 en count2 zich waarschijnlijk op dezelfde cacheregel bevinden.

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;

Een manier om valse deling te elimineren, is door ervoor te zorgen dat de tellervariabelen zich op afzonderlijke cache-lijnen bevinden. In het volgende voorbeeld worden de variabelen count1 en count2 uitgelijnd op grenzen van 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;

In dit voorbeeld wordt ervan uitgegaan dat de grootte van de geheugencache 64 of minder bytes is.

U wordt aangeraden de gelijktijdigheid::combineerbare klasse te gebruiken wanneer u gegevens tussen taken moet delen. De combinable klasse maakt thread-lokale variabelen op een zodanige manier dat onwaar delen minder waarschijnlijk is. Zie combinable voor meer informatie over de klasse.

[Boven]

Zorg ervoor dat variabelen gedurende de levensduur van een taak geldig zijn

Wanneer u een lambda-expressie aan een taakgroep of parallel algoritme opgeeft, specificeert de capture-clausule of de body van de lambda-expressie toegang heeft tot variabelen in de omgeving op waarde of op verwijzing. Wanneer u variabelen naar een lambda-expressie doorgeeft, moet u garanderen dat de levensduur van die variabele blijft bestaan totdat de taak is voltooid.

Bekijk het volgende voorbeeld waarmee de object klasse en de perform_action functie worden gedefinieerd. De perform_action functie maakt een object variabele en voert een actie uit op die variabele asynchroon. Omdat de taak niet gegarandeerd is voltooid voordat de perform_action functie wordt geretourneerd, loopt het programma vast of vertoont het niet-opgegeven gedrag als de object variabele wordt vernietigd wanneer de taak wordt uitgevoerd.

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

Afhankelijk van de vereisten van uw toepassing kunt u een van de volgende technieken gebruiken om te garanderen dat variabelen gedurende de levensduur van elke taak geldig blijven.

In het volgende voorbeeld wordt de object variabele op waarde doorgegeven aan de taak. Daarom werkt de taak op een eigen kopie van de variabele.

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

Omdat de object variabele wordt doorgegeven door een waarde, worden alle statuswijzigingen die in deze variabele optreden, niet weergegeven in de oorspronkelijke kopie.

In het volgende voorbeeld wordt de concurrentie::task_group::wait-methode gebruikt om ervoor te zorgen dat de taak wordt voltooid voordat de perform_action functie wordt geretourneerd.

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

Omdat de taak nu is voltooid voordat de functie wordt geretourneerd, gedraagt de perform_action functie zich niet meer asynchroon.

In het volgende voorbeeld wordt de perform_action functie gewijzigd om een verwijzing naar de object variabele te maken. De aanroeper moet garanderen dat de levensduur van de object variabele geldig is totdat de taak is voltooid.

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

U kunt ook een aanwijzer gebruiken om de levensduur van een object te bepalen dat u doorgeeft aan een taakgroep of parallel algoritme.

Zie Lambda-expressies voor meer informatie over lambda-expressies.

[Boven]

Zie ook

Best practices voor Concurrency Runtime
Bibliotheek met parallelle patronen (PPL)
parallelle containers en objecten
Parallelle algoritmen
Annulering in de PPL
afhandeling van uitzonderingen
Overzicht: Een Image-Processing-netwerk maken
Procedure: parallel_invoke gebruiken om een parallelle sorteerroutine te schrijven
Procedure: Annulering gebruiken om een parallelle lus te verbreken
Procedure: Combineerbaar gebruiken om de prestaties te verbeteren
Aanbevolen procedures in de bibliotheek met Asynchrone agents
Algemene aanbevolen procedures in de Gelijktijdigheidsruntime