Dela via


Bästa metoder i biblioteket för parallella mönster

Det här dokumentet beskriver hur du bäst använder PPL (Parallel Patterns Library) på ett effektivt sätt. PPL tillhandahåller containrar, objekt och algoritmer för generell användning för att utföra detaljerad parallellitet.

Mer information om PPL finns i PPL (Parallel Patterns Library).

Sektioner

Det här dokumentet innehåller följande avsnitt:

Parallellisera inte små loopar

Parallelliseringen av relativt små loopar kan göra att de associerade schemaläggningskostnaderna uppväger fördelarna med parallell bearbetning. Tänk på följande exempel, som lägger till varje elementpar i två matriser.

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

Arbetsbelastningen för varje parallell loop-iteration är för liten för att dra nytta av kostnaderna för parallell bearbetning. Du kan förbättra prestandan för den här loopen genom att utföra mer arbete i loopens brödtext eller genom att utföra loopen seriellt.

[Topp]

Expressparallellitet på högsta möjliga nivå

När du bara parallelliserar kod på den låga nivån kan du introducera en förgreningskonstruktion som inte skalas när antalet processorer ökar. En fork-join-konstruktion är en konstruktion där en aktivitet delar upp sitt arbete i mindre parallella underaktiviteter och väntar på att dessa underaktiviteter ska slutföras. Varje underavdelning kan rekursivt dela upp sig i ytterligare underaktiviteter.

Även om förgreningskopplingsmodellen kan vara användbar för att lösa en mängd olika problem, finns det situationer där synkroniseringskostnaderna kan minska skalbarheten. Tänk till exempel på följande seriekod som bearbetar bilddata.

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

Eftersom varje loop-iteration är oberoende kan du parallellisera mycket av arbetet, som du ser i följande exempel. I det här exemplet används algoritmen concurrency::parallel_for för att parallellisera den yttre slingan.

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

I följande exempel visas en fork-join-konstruktion genom att anropa ProcessImage funktionen i en loop. Varje anrop till ProcessImage returneras inte förrän varje underavdelning har slutförts.

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

Om varje iteration av den parallella loopen antingen utför nästan inget arbete, eller om det arbete som utförs av den parallella loopen är obalanserat, dvs. vissa loop-iterationer tar längre tid än andra, kan schemaläggningskostnaderna som krävs för att ofta förgrena och ansluta arbete uppväga fördelen med parallell körning. Den här kostnaden ökar i takt med att antalet processorer ökar.

Om du vill minska mängden schemaläggningskostnader i det här exemplet kan du parallellisera yttre loopar innan du parallelliserar inre slingor eller använder en annan parallell konstruktion, till exempel pipelining. Det följande exemplet ändrar funktionen ProcessImages så att den använder algoritmen concurrency::parallel_for_each för att parallellisera den yttre loopen.

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

Ett liknande exempel som använder en pipeline för att utföra bildbearbetning parallellt finns i Genomgång: Skapa ett Image-Processing nätverk.

[Topp]

Använd parallel_invoke för att lösa problem med del-och-härska-metoden

Ett delas och härska-problem är en form av fork-join-konstruktion som använder rekursion för att dela upp en uppgift i deluppgifter. Förutom klasserna concurrency::task_group och concurrency::structured_task_group kan du också använda algoritmen concurrency::parallel_invoke för att lösa problem med divide-and-conquer. Algoritmen parallel_invoke har en mer kortfattad syntax än aktivitetsgruppsobjekt och är användbar när du har ett fast antal parallella aktiviteter.

I följande exempel visas hur algoritmen parallel_invoke används för att implementera bitonisk sorteringsalgoritm.

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

För att minska kostnaderna utför algoritmen parallel_invoke den sista av serien med uppgifter i anropskontexten.

Den fullständiga versionen av det här exemplet finns i How to: Use parallel_invoke to Write a Parallel Sort Routine (Använda parallel_invoke för att skriva en parallell sorteringsrutin). Mer information om algoritmen finns i parallel_invokeParallella algoritmer.

[Topp]

Använd Annullering eller undantagshantering för att bryta från en parallell loop

PPL ger två sätt att avbryta det parallella arbete som utförs av en aktivitetsgrupp eller parallell algoritm. Ett sätt är att använda den annulleringsmekanism som tillhandahålls av konkurrens::task_group och konkurrens::structured_task_group-klasserna. Det andra sättet är att utlösa ett undantag i brödtexten för en uppgiftsarbetsfunktion. Annulleringsmekanismen är effektivare vid anullering av en struktur av parallellt arbete än undantagshantering. Ett parallellt arbetsträd är en grupp relaterade aktivitetsgrupper där vissa aktivitetsgrupper innehåller andra aktivitetsgrupper. Annulleringsmekanismen annullerar en aktivitetsgrupp och dess underordnade aktivitetsgrupper uppifrån och ned. Omvänt fungerar undantagshantering nedifrån och upp och måste avbryta varje underordnad aktivitetsgrupp oberoende medan undantaget sprids uppåt.

När du arbetar direkt med ett aktivitetsgruppobjekt använder du samtidighet::task_group::avbryt eller samtidighet::structured_task_group::avbryt för att avbryta det arbete som tillhör den aktivitetsgruppen. För att avbryta en parallell algoritm, till exempel parallel_for, skapa en överordnad aktivitetsgrupp och avbryt aktivitetsgruppen. Tänk dig till exempel följande funktion, parallel_find_any, som söker efter ett värde i en matris parallellt.

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

Eftersom parallella algoritmer använder aktivitetsgrupper avbryts den övergripande aktiviteten när en av de parallella iterationerna avbryter den överordnade aktivitetsgruppen. Den fullständiga versionen av det här exemplet finns i How to: Use Cancellation to Break from a Parallel Loop (Så här gör du: Använd Annullering för att bryta från en parallell loop).

Även om undantagshantering är ett mindre effektivt sätt att avbryta parallellt arbete än avbokningsmekanismen, finns det fall där undantagshantering är lämpligt. Följande metod, for_all, utför rekursivt en arbetsfunktion på varje nod i en tree struktur. I det här exemplet _children är datamedlemmen en std::list som innehåller tree objekt.

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

Anroparen för tree::for_all metoden kan utlösa ett undantag om den inte kräver att arbetsfunktionen anropas för varje element i trädet. I följande exempel visas funktionen search_for_value som söker efter ett värde i det angivna tree objektet. Funktionen search_for_value använder en arbetsfunktion som utlöser ett undantag när det aktuella elementet i trädet matchar det angivna värdet. Funktionen search_for_value använder ett try-catch block för att avbilda undantaget och skriva ut resultatet till konsolen.

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

Den fullständiga versionen av det här exemplet finns i How to: Use Exception Handling to Break from a Parallel Loop (Använda undantagshantering för att bryta från en parallell loop).

Mer allmän information om mekanismerna för annullering och undantagshantering som tillhandahålls av PPL finns i Annullering i PPL och undantagshantering.

[Topp]

Förstå hur annullering och undantagshantering påverkar objektförstörelse

I ett träd med parallellt arbete förhindrar en uppgift som avbryts underliggande uppgifter från att köras. Detta kan orsaka problem om en av de underordnade uppgifterna utför en åtgärd som är viktig för ditt program, till exempel att frigöra en resurs. Dessutom kan aktivitetsavbokning orsaka ett undantag som sprids via en objektförstörare och orsaka odefinierat beteende i ditt program.

I följande exempel Resource beskriver klassen en resurs och Container klassen beskriver en container som innehåller resurser. I sin destruktor anropar klassen Container-metoden på två av sina cleanup-medlemmar parallellt och anropar sedan Resource-metoden på sin tredje cleanup-medlem.

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

Även om det här mönstret inte har några problem på egen hand bör du överväga följande kod som kör två uppgifter parallellt. Den första aktiviteten skapar ett Container objekt och den andra aktiviteten avbryter den övergripande aktiviteten. Till illustration använder exemplet två concurrency::event objekt för att se till att annulleringen sker efter att Container objektet har skapats och att Container objektet förstörs efter att annulleringsåtgärden har inträffat.

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

Det här exemplet genererar följande utdata:

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

Det här kodexemplet innehåller följande problem som kan leda till att det fungerar annorlunda än förväntat:

  • Om den överordnade aktiviteten avbryts orsakar det att även den underordnade aktiviteten, anropet till concurrency::parallel_invoke, avbryts. Därför frigörs inte dessa två resurser.

  • Om den överordnade aktiviteten avbryts utlöser den underordnade aktiviteten ett internt undantag. Container Eftersom destructor inte hanterar det här undantaget sprids undantaget uppåt och den tredje resursen frigörs inte.

  • Undantaget som kastas av underuppgiften sprids via destruktörenContainer. Om du kastar från en destructor förser du programmet med ett odefinierat tillstånd.

Vi rekommenderar att du inte utför kritiska åtgärder, till exempel frigörande av resurser, i aktiviteter om du inte kan garantera att dessa uppgifter inte avbryts. Vi rekommenderar också att du inte använder körningsfunktioner som kan utlösa destruktor för dina typer.

[Topp]

Blockera inte flera gånger i en parallell loop

En parallell loop som concurrency::parallel_for eller concurrency::parallel_for_each som domineras av blockeringsåtgärder kan orsaka att runtime skapar många trådar på kort tid.

Concurrency Runtime utför ytterligare arbete när en uppgift slutförs eller kooperativt blockerar eller ger resultat. När en parallell loop-iteration blockeras kan körsystemet påbörja en annan iteration. När det inte finns några tillgängliga inaktiva trådar skapar körningen en ny tråd.

När kroppen av en parallell loop tillfälligt blockeras, hjälper den här mekanismen till att maximera den övergripande uppgiftens genomströmning. Men när många iterationer blockeras kommer körmiljön att skapa många trådar för att köra extra arbete. Detta kan leda till minnesbrist eller dålig användning av maskinvaruresurser.

Tänk dig följande exempel som anropar funktionen concurrency::send i varje iteration av en parallel_for loop. Eftersom send blockerar samarbete skapar körningen en ny tråd för att köra ytterligare arbete varje gång send anropas.

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

Vi rekommenderar att du omstrukturerar koden för att undvika det här mönstret. I det här exemplet kan du undvika att skapa ytterligare trådar genom att anropa send i en seriell for loop.

[Topp]

Utför inte blockeringsåtgärder när du avbryter parallellt arbete

När det är möjligt ska du inte utföra blockerande operationer innan du anropar concurrency::task_group::cancel eller concurrency::structured_task_group::cancel-metoden för att avbryta parallellt arbete.

När en uppgift utför en kooperativ blockering kan körmiljön utföra annat arbete medan den första uppgiften väntar på data. Körtiden schemalägger om den väntande uppgiften när den avblockeras. Körtiden schemalägger typiskt om aktiviteter som nyligen avblockerades, före aktiviteter som avblockerades tidigare. Därför kan programkörningsmiljön schemalägga onödigt arbete under en blockeringsåtgärd, vilket leder till sämre prestanda. När du utför en blockeringsåtgärd innan du avbryter det parallella arbetet kan därför blockeringsåtgärden fördröja anropet till cancel. Detta gör att andra uppgifter utför onödigt arbete.

Tänk dig följande exempel som definierar parallel_find_answer funktionen, som söker efter ett element i den angivna matrisen som uppfyller den angivna predikatfunktionen. När predikatfunktionen returnerar trueskapar den parallella arbetsfunktionen ett Answer objekt och avbryter den övergripande aktiviteten.

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

Operatorn new utför en heapallokering som kan göra att programmet blockeras. Körtiden utför endast annat arbete när en uppgift utför ett samverkande blockeringsanrop, till exempel ett anrop till samtidighet::critical_section::lock.

I följande exempel visas hur du förhindrar onödigt arbete och därmed förbättrar prestandan. Det här exemplet avbryter aktivitetsgruppen innan den allokerar lagringen Answer för objektet.

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

[Topp]

Skriv inte till delade data i en parallell loop

Concurrency Runtime innehåller flera datastrukturer, till exempel concurrency::critical_section, som synkroniserar samtidig åtkomst till delad data. Dessa datastrukturer är användbara i många fall, till exempel när flera uppgifter sällan kräver delad åtkomst till en resurs.

Tänk på följande exempel som använder algoritmen concurrency::p arallel_for_each och ett critical_section objekt för att beräkna antalet primtal i ett std::array-objekt . Det här exemplet skalas inte eftersom varje tråd måste vänta för att få åtkomst till den delade variabeln 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();
});

Det här exemplet kan också leda till dåliga prestanda eftersom den frekventa låsåtgärden effektivt serialiserar loopen. När ett Concurrency Runtime-objekt utför en blockeringsåtgärd kan schemaläggaren dessutom skapa ytterligare en tråd för att utföra annat arbete medan den första tråden väntar på data. Om körtiden skapar många trådar eftersom många uppgifter väntar på delade data kan programmet ha dålig prestanda eller hamna i ett läge med låg resursnivå.

PPL definierar klassen concurrency::combinable , som hjälper dig att eliminera delat tillstånd genom att ge åtkomst till delade resurser på ett låsfritt sätt. Klassen combinable tillhandahåller trådlokal lagring där du kan utföra detaljerade beräkningar och sedan sammanfoga dessa beräkningar till ett slutligt resultat. Du kan se ett combinable objekt som en minskningsvariabel.

I följande exempel ändras det föregående med hjälp av ett combinable objekt i stället för ett critical_section objekt för att beräkna summan. Det här exemplet skalas eftersom varje tråd innehåller en egen lokal kopia av summan. I det här exemplet används metoden concurrency::combinable::combine för att sammanfoga de lokala beräkningarna till slutresultatet.

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

Den fullständiga versionen av det här exemplet finns i How to: Use combinable to Improve Performance (Så här gör du: Använd kombinerbart för att förbättra prestanda). Mer information om klassen finns i combinableParallella containrar och objekt.

[Topp]

Undvik falsk delning när det är möjligt

Falsk delning sker när flera samtidiga uppgifter som körs på separata processorer skriver till variabler som finns på samma cacherad. När en uppgift skriven i en cache skriver till en av variablerna blir cacheraden för båda variablerna ogiltig. Varje processor måste läsa in cacheraden igen varje gång cacheraden är ogiltig. Därför kan falsk delning orsaka sämre prestanda i ditt program.

Följande grundläggande exempel visar två samtidiga uppgifter som var och en ökar en delad räknarvariabel.

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

Om du vill eliminera datadelningen mellan de två uppgifterna kan du ändra exemplet så att det använder två räknarvariabler. I det här exemplet beräknas det slutliga räknarvärdet när aktiviteterna har slutförts. Det här exemplet illustrerar dock falsk delning eftersom variablerna count1 och count2 sannolikt kommer att finnas på samma cacherad.

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;

Ett sätt att eliminera falsk delning är att se till att räknarvariablerna finns på separata cacherader. I följande exempel justeras variablerna count1 och count2 på 64 bytes gränser.

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

Det här exemplet förutsätter att storleken på minnescachen är 64 eller färre byte.

Vi rekommenderar att du använder klassen concurrency::combinable när du måste dela data mellan aktiviteter. Klassen combinable skapar trådlokala variabler på ett sådant sätt att falsk delning är mindre sannolikt. Mer information om klassen finns i combinableParallella containrar och objekt.

[Topp]

Kontrollera att variablerna är giltiga under en aktivitets livslängd

När du anger ett lambda-uttryck för en aktivitetsgrupp eller en parallell algoritm, anger fångstklausulen om lambda-uttryckets kropp kommer åt variabler i det omgivande omfånget genom värde eller referens. När du skickar variabler till ett lambda-uttryck som referens måste du garantera att variabelns livslängd kvarstår tills aktiviteten har slutförts.

Tänk dig följande exempel som definierar object klassen och perform_action funktionen. Funktionen perform_action skapar en object variabel och utför en viss åtgärd på variabeln asynkront. Eftersom aktiviteten inte garanteras att slutföras innan perform_action funktionen returneras kraschar eller uppvisar programmet ett ospecificerat beteende om variabeln object förstörs när aktiviteten körs.

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

Beroende på kraven i ditt program kan du använda någon av följande tekniker för att garantera att variablerna förblir giltiga under hela livslängden för varje uppgift.

I följande exempel skickas variabeln object efter värde till uppgiften. Därför fungerar aktiviteten på en egen kopia av variabeln.

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

Eftersom variabeln object skickas efter värde visas inga tillståndsändringar som inträffar i den här variabeln i den ursprungliga kopian.

I följande exempel används metoden concurrency::task_group::wait för att se till att aktiviteten slutförs innan perform_action funktionen returneras.

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

Eftersom aktiviteten nu slutförs innan funktionen returneras fungerar inte perform_action längre funktionen asynkront.

I följande exempel ändras funktionen så att den perform_action refererar till variabeln object . Anroparen måste garantera att variabelns object livslängd är giltig tills aktiviteten har slutförts.

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

Du kan också använda en pekare för att styra livslängden för ett objekt som du skickar till en aktivitetsgrupp eller en parallell algoritm.

Mer information om lambda-uttryck finns i Lambda-uttryck.

[Topp]

Se även

Bästa praxis för samtidighetskörning
PPL (Parallel Patterns Library)
parallella containrar och objekt
Parallella algoritmer
Annullering i PPL
undantagshantering
Genomgång: Skapa ett Image-Processing nätverk
Gör så här: Använd parallel_invoke för att skriva en parallell sorteringsrutin
Anvisningar: Använda Annullering för att bryta från en parallell loop
Gör så här: Använd kombinerbart för att förbättra prestanda
Metodtips i biblioteket för asynkrona agenter
Allmänna metodtips i Concurrency Runtime