Osvědčené postupy v knihovně PPL (Parallel Patterns Library)

Tento dokument popisuje, jak nejlépe využít knihovnu PPL (Parallel Patterns Library). PPL poskytuje kontejnery, objekty a algoritmy pro obecné účely pro provádění jemně odstupňovaného paralelismu.

Další informace o PPL naleznete v tématu Parallel Patterns Library (PPL).

Oddíly

Tento dokument obsahuje následující části:

Ne parallelizace malých smyček

Paralelizace relativně malých smyček může způsobit, že související režie plánování převáží nad výhodami paralelního zpracování. Podívejte se na následující příklad, který přidá každou dvojici prvků do dvou polí.

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

Úloha pro každou iteraci paralelní smyčky je příliš malá, aby využívala režijní náklady na paralelní zpracování. Výkon této smyčky můžete zlepšit provedením větší práce v těle smyčky nebo provedením smyčky sériově.

[Nahoře]

Vyjádření paralelismu na nejvyšší možné úrovni

Když paralelizujete kód pouze na nízké úrovni, můžete zavést fork-join konstruktor, který se neškuluje při nárůstu počtu procesorů. Konstruktor fork-join je konstrukce, kde jeden úkol rozdělí svou práci na menší paralelní dílčí úkoly a čeká na dokončení těchto dílčích úkolů. Každý dílčí úkol se může rekurzivně rozdělit do dalších dílčích úkolů.

I když model fork-join může být užitečný pro řešení různých problémů, existují situace, kdy režie synchronizace může snížit škálovatelnost. Představte si například následující sériový kód, který zpracovává data obrázků.

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

Vzhledem k tomu, že každá iterace smyčky je nezávislá, můžete paralelizovat většinu práce, jak je znázorněno v následujícím příkladu. Tento příklad používá souběžnost::p arallel_for algoritmu k paralelizaci vnější smyčky.

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

Následující příklad znázorňuje fork-join konstruktor voláním ProcessImage funkce ve smyčce. Každé volání ProcessImage se nevrátí, dokud se nedokončí každý dílčí úkol.

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

Pokud každá iterace paralelní smyčky buď provádí téměř žádnou práci, nebo práce, kterou provádí paralelní smyčka, je nevyvážená, to znamená, že některé iterace smyčky trvá déle než jiné, plánování režie, která je nutná k častému rozvětvování a spojení práce, může převažovat nad výhodou paralelního provádění. Tato režie se zvyšuje při nárůstu počtu procesorů.

Chcete-li snížit množství režijních nákladů na plánování v tomto příkladu, můžete paralelizovat vnější smyčky před paralelizací vnitřních smyček nebo použít jiný paralelní konstruktor, například pipelining. Následující příklad upraví ProcessImages funkci tak, aby používala souběžnost::p arallel_for_each algoritmu k paralelizaci vnější smyčky.

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

Podobný příklad, který používá kanál k paralelnímu zpracování obrázků, najdete v tématu Návod: Vytvoření sítě pro zpracování obrázků.

[Nahoře]

Použití parallel_invoke k řešení problémů s dělením a dobytím

Problém dělení a dobytí je forma konstruktoru fork-join, který pomocí rekurze rozdělí úkol na dílčí úkoly. Kromě tříd concurrency::task_group a concurrency::structured_task_group můžete také použít algoritmus concurrency::p arallel_invoke k řešení problémů s dělením a dobízením. Algoritmus parallel_invoke má stručnější syntaxi než objekty skupiny úloh a je užitečný, pokud máte pevný počet paralelních úloh.

Následující příklad znázorňuje použití parallel_invoke algoritmu k implementaci algoritmu řazení bitových hodnot.

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

Aby se snížila režie, parallel_invoke algoritmus provádí poslední řadu úloh na volajícím kontextu.

Úplnou verzi tohoto příkladu najdete v tématu Postupy: Použití parallel_invoke k zápisu rutiny paralelního řazení. Další informace o parallel_invoke algoritmu naleznete v tématu Paralelní algoritmy.

[Nahoře]

Přerušení paralelní smyčky pomocí zpracování zrušení nebo výjimek

PPL nabízí dva způsoby zrušení paralelní práce prováděné skupinou úloh nebo paralelním algoritmem. Jedním ze způsobů je použití mechanismu zrušení, který poskytuje souběžnost::task_group a souběžnost::structured_task_group třídy. Druhým způsobem je vyvolání výjimky v těle pracovní funkce úkolu. Mechanismus zrušení je efektivnější než zpracování výjimek při rušení stromu paralelní práce. Paralelní pracovní strom je skupina souvisejících skupin úkolů, ve kterých některé skupiny úkolů obsahují jiné skupiny úkolů. Mechanismus zrušení zruší skupinu úloh a její podřízené skupiny úkolů shora dolů. Zpracování výjimek naopak funguje směrem dolů a musí zrušit každou podřízenou skupinu úloh nezávisle, protože se výjimka šíří směrem nahoru.

Když pracujete přímo s objektem skupiny úloh, použijte souběžnost::task_group::cancel nebo concurrency::structured_task_group::cancel metody zrušit práci, která patří do této skupiny úloh. Pokud chcete například parallel_forzrušit paralelní algoritmus, vytvořte nadřazenou skupinu úloh a zrušte ji. Představte si například následující funkci, parallel_find_anykterá hledá hodnotu v poli paralelně.

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

Vzhledem k tomu, že paralelní algoritmy používají skupiny úloh, když jedna z paralelních iterací zruší nadřazenou skupinu úloh, zruší se celkový úkol. Kompletní verzi tohoto příkladu najdete v tématu Postupy: Přerušení paralelní smyčky pomocí zrušení.

I když zpracování výjimek představuje méně efektivní způsob zrušení paralelní práce než mechanismus zrušení, existují případy, kdy je zpracování výjimek vhodné. Například následující metoda rekurzivně for_allprovádí pracovní funkci na každém uzlu tree struktury. V tomto příkladu _children je datovým členem std::list , který obsahuje tree objekty.

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

Volající tree::for_all metody může vyvolat výjimku, pokud nevyžaduje volání pracovní funkce pro každý prvek stromu. Následující příklad ukazuje search_for_value funkci, která hledá hodnotu v zadaném tree objektu. Funkce search_for_value používá pracovní funkci, která vyvolá výjimku, když aktuální prvek stromu odpovídá zadané hodnotě. Funkce search_for_value používá try-catch blok k zachycení výjimky a tisku výsledku do konzoly.

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

Kompletní verzi tohoto příkladu najdete v tématu Postupy: Použití zpracování výjimek k přerušení paralelní smyčky.

Další obecné informace o mechanismech zrušení a zpracování výjimek, které poskytuje PPL, naleznete v tématu Zrušení v PPL a Zpracování výjimek.

[Nahoře]

Vysvětlení vlivu zpracování zrušení a výjimek na zničení objektů

Ve stromu paralelní práce úloha, která je zrušena, zabraňuje spuštění podřízených úkolů. To může způsobit problémy v případě, že jedna z podřízených úloh provádí operaci, která je pro vaši aplikaci důležitá, například uvolnění prostředku. Zrušení úlohy může navíc způsobit šíření výjimky prostřednictvím destruktoru objektu a způsobit nedefinované chování ve vaší aplikaci.

V následujícím příkladu Resource třída popisuje prostředek a Container třída popisuje kontejner, který obsahuje prostředky. V jeho destruktoru Container třída volá metodu cleanup na dvou ze svých Resource členů paralelně a pak volá metodu cleanup na jeho třetí Resource člen.

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

I když tento model nemá žádné problémy samostatně, zvažte následující kód, který spouští dva úlohy paralelně. První úkol vytvoří Container objekt a druhý úkol zruší celkový úkol. Na obrázku příklad používá dva objekty concurrency::event , aby se zajistilo, že zrušení proběhne po Container vytvoření objektu a že Container je objekt zničen po dokončení operace zrušení.

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

Tento příklad vytvoří následující výstup:

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

Tento příklad kódu obsahuje následující problémy, které můžou způsobit, že se chová jinak, než očekáváte:

  • Zrušení nadřazené úlohy způsobí, že se zruší také podřízený úkol, volání souběžnosti::p arallel_invoke. Proto tyto dva prostředky nejsou uvolněny.

  • Zrušení nadřazené úlohy způsobí, že podřízený úkol vyvolá vnitřní výjimku. Container Vzhledem k tomu, že destruktor tuto výjimku nezpracuje, je výjimka rozšířena směrem nahoru a třetí prostředek není uvolněn.

  • Výjimka vyvolaná podřízeným úkolem se šíří prostřednictvím Container destruktoru. Vyvolání z destruktoru umístí aplikaci do nedefinovaného stavu.

Pokud nezaručíte, že tyto úkoly nebudou zrušeny, doporučujeme provádět kritické operace, jako je uvolnění zdrojů. Doporučujeme také nepoužívat funkce modulu runtime, které můžou vyvolat destruktor vašich typů.

[Nahoře]

Neblokovat opakovaně v paralelní smyčce

Paralelní smyčka, jako je concurrency::p arallel_for nebo concurrency::p arallel_for_each , která dominuje blokováním operací, může způsobit, že modul runtime během krátké doby vytvoří mnoho vláken.

Modul Concurrency Runtime provádí další práci, když se úkol dokončí nebo spolupracuje s bloky nebo výnosy. Pokud jeden paralelní iterační blok smyčky, modul runtime může zahájit další iteraci. Pokud nejsou k dispozici žádná nečinná vlákna, modul runtime vytvoří nové vlákno.

Když tělo paralelní smyčky občas blokuje, tento mechanismus pomáhá maximalizovat celkovou propustnost úloh. Pokud však blokuje mnoho iterací, může modul runtime vytvořit mnoho vláken pro spuštění další práce. To může vést k nedostatku paměti nebo špatnému využití hardwarových prostředků.

Představte si následující příklad, který volá funkci concurrency::send v každé iteraci smyčky parallel_for . Vzhledem k tomu send , že bloky spolupracují, modul runtime vytvoří nové vlákno pro spuštění další práce při send každém volání.

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

Doporučujeme refaktorovat kód, abyste se tomuto vzoru vyhnuli. V tomto příkladu se můžete vyhnout vytváření dalších vláken voláním send v sériové for smyčce.

[Nahoře]

Neprovádějte blokující operace při zrušení paralelní práce

Pokud je to možné, neprovádějte blokující operace před voláním souběžnosti::task_group::cancel nebo concurrency::structured_task_group::cancel metoda pro zrušení paralelní práce.

Když úloha provede operaci blokování spolupráce, může modul runtime provést jinou práci, zatímco první úloha čeká na data. Modul runtime přeplánuje čekající úlohu při odblokování. Modul runtime obvykle přeplánuje úlohy, které byly nedávno odblokovány dříve, než přeplánoval úkoly, které byly odblokovány méně nedávno. Modul runtime by proto mohl naplánovat zbytečnou práci během blokující operace, což vede ke snížení výkonu. Pokud tedy provedete blokující operaci před zrušením paralelní práce, může operace blokování zpozdit volání cancel. To způsobí, že jiné úlohy budou provádět nepotřebnou práci.

Podívejte se na následující příklad, který definuje parallel_find_answer funkci, která hledá prvek poskytnuté pole, který splňuje zadanou predikátovou funkci. Když se predikát vrátí true, vytvoří paralelní pracovní funkce Answer objekt a zruší celkový úkol.

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

Operátor new provede přidělení haldy, která může blokovat. Modul runtime provádí jinou práci pouze v případě, že úloha provádí spolupracující blokující volání, například volání souběžnosti::critical_section::lock.

Následující příklad ukazuje, jak zabránit zbytečné práci a zlepšit tak výkon. Tento příklad zruší skupinu úloh před přidělením úložiště pro Answer objekt.

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

[Nahoře]

Nezapisovat do sdílených dat v paralelní smyčce

Concurrency Runtime poskytuje několik datových struktur, například concurrency::critical_section, které synchronizují souběžný přístup ke sdíleným datům. Tyto datové struktury jsou užitečné v mnoha případech, například když několik úloh zřídka vyžaduje sdílený přístup k prostředku.

Podívejte se na následující příklad, který používá souběžnost::p arallel_for_each algoritmus a critical_section objekt k výpočtu počtu prime čísel v objektu std::array . Tento příklad není škálovat, protože každé vlákno musí čekat na přístup ke sdílené proměnné 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();
});

Tento příklad může také vést k nízkému výkonu, protože často zamykací operace efektivně serializuje smyčku. Kromě toho, když objekt Concurrency Runtime provádí blokující operaci, plánovač může vytvořit další vlákno, které provede další práci, zatímco první vlákno čeká na data. Pokud modul runtime vytvoří mnoho vláken, protože mnoho úloh čeká na sdílená data, může aplikace provádět špatně nebo zadávat stav s nízkými prostředky.

PPL definuje souběžnost::combinable třída, která pomáhá eliminovat sdílený stav tím, že poskytuje přístup ke sdíleným prostředkům bez uzamčení. Třída combinable poskytuje místní úložiště vláken, které umožňuje provádět jemně odstupňované výpočty a pak tyto výpočty sloučit do konečného výsledku. Objekt si můžete představit combinable jako proměnnou redukce.

Následující příklad upraví předchozí objekt pomocí objektu combinable místo critical_section objektu pro výpočet součtu. Tento příklad se škáluje, protože každé vlákno obsahuje vlastní místní kopii součtu. Tento příklad používá metodu concurrency::combinable::combine k sloučení místních výpočtů do konečného výsledku.

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

Kompletní verzi tohoto příkladu najdete v tématu Postupy: Použití kombinace ke zlepšení výkonu. Další informace o combinable třídě naleznete v tématu Paralelní kontejnery a objekty.

[Nahoře]

Pokud je to možné, vyhněte se sdílení nepravda.

Sdílení nepravda nastane, když několik souběžných úloh spuštěných na samostatných procesorech zapisuje do proměnných umístěných na stejném řádku mezipaměti. Když jeden úkol zapíše do jedné z proměnných, řádek mezipaměti pro obě proměnné se zneplatní. Každý procesor musí znovu načíst řádek mezipaměti pokaždé, když je řádek mezipaměti neplatný. Proto může sdílení nepravda způsobit snížení výkonu ve vaší aplikaci.

Následující základní příklad ukazuje dva souběžné úlohy, které každý zvýší sdílenou proměnnou čítače.

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

Chcete-li odstranit sdílení dat mezi dvěma úkoly, můžete upravit příklad tak, aby používal dvě proměnné čítače. Tento příklad vypočítá konečnou hodnotu čítače po dokončení úkolů. Tento příklad ale ukazuje sdílení nepravda, protože proměnné count1 a count2 pravděpodobně se nacházejí na stejném řádku mezipaměti.

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;

Jedním ze způsobů, jak eliminovat sdílení nepravda, je zajistit, aby proměnné čítačů byly na samostatných řádcích mezipaměti. Následující příklad zarovná proměnné count1 a count2 hranice 64 bajtů.

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

Tento příklad předpokládá, že velikost mezipaměti paměti je 64 nebo méně bajtů.

Pokud potřebujete sdílet data mezi úkoly, doporučujeme použít třídu concurrency::combinable . Třída combinable vytváří proměnné typu thread-local takovým způsobem, že sdílení nepravda je méně pravděpodobné. Další informace o combinable třídě naleznete v tématu Paralelní kontejnery a objekty.

[Nahoře]

Ujistěte se, že proměnné jsou platné po celou dobu životnosti úkolu.

Když zadáte výraz lambda skupině úloh nebo paralelnímu algoritmu, klauzule capture určuje, jestli text výrazu lambda přistupuje k proměnným v uzavřeném oboru podle hodnoty nebo odkazu. Když předáte proměnné výrazu lambda odkazem, musíte zaručit, že životnost této proměnné přetrvává, dokud se úkol nedokončí.

Podívejte se na následující příklad, který definuje object třídu a perform_action funkci. Funkce perform_action vytvoří proměnnou object a provede nějakou akci s danou proměnnou asynchronně. Vzhledem k tomu, že úloha není zaručena dokončení před perform_action vrácením funkce, program selže nebo vykazuje nezadané chování, pokud object je proměnná zničena při spuštění úkolu.

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

V závislosti na požadavcích vaší aplikace můžete pomocí jedné z následujících technik zaručit, že proměnné zůstanou platné po celou dobu životnosti každého úkolu.

Následující příklad předá object proměnnou hodnotou úkolu. Proto úloha pracuje s vlastní kopií proměnné.

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

object Vzhledem k tomu, že proměnná je předána hodnotou, všechny změny stavu, ke kterým dojde k této proměnné, se v původní kopii nezobrazí.

Následující příklad používá concurrency::task_group::wait metoda, aby se zajistilo dokončení úlohy před vrácením perform_action funkce.

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

Vzhledem k tomu, že se úkol dokončí před vrácením funkce, perform_action funkce se už nechová asynchronně.

Následující příklad upraví perform_action funkci tak, aby odkaz na proměnnou object . Volající musí zaručit platnost životnosti object proměnné, dokud se úkol nedokončí.

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

Ukazatel můžete také použít k řízení životnosti objektu, který předáte skupině úloh nebo paralelnímu algoritmu.

Další informace o výrazech lambda najdete v tématu Výrazy lambda.

[Nahoře]

Viz také

Osvědčené postupy v Concurrency Runtime
Knihovna PPL (Parallel Patterns Library)
Paralelní kontejnery a objekty
Paralelní algoritmy
Zrušení v knihovně PPL
Zpracování výjimek
Návod: Vytvoření sítě pro zpracování obrázků
Postupy: Použití algoritmu parallel_invoke k zápisu rutiny paralelního třídění
Postupy: Přerušení paralelní smyčky pomocí zrušení
Postupy: Použití objektu combinable ke zlepšení výkonu
Osvědčené postupy v knihovně asynchronních agentů
Obecné osvědčené postupy v Concurrency Runtime