Udostępnij za pośrednictwem


Biblioteka wzorów równoległych — Najlepsze praktyki

W tym dokumencie opisano, jak najlepiej wykorzystać bibliotekę równoległych wzorców (PPL). PPL zapewnia kontenery ogólnego przeznaczenia, obiekty i algorytmy do wykonywania precyzyjnego równoległości.

Aby uzyskać więcej informacji na temat PPL, zobacz Parallel Patterns Library (PPL).

Sekcje

Ten dokument zawiera następujące sekcje:

Nie równoległe małe jednostki pętli

Równoległość stosunkowo małych jednostek pętli może spowodować, że związane z tym obciążenie związane z planowaniem przewyższa korzyści wynikające z przetwarzania równoległego. Rozważmy poniższy przykład, który dodaje każdą parę elementów w dwóch tablicach.

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

Obciążenie dla każdej iteracji pętli równoległej jest zbyt małe, aby korzystać z obciążenia związanego z przetwarzaniem równoległym. Wydajność tej pętli można poprawić, wykonując większą pracę w treści pętli lub wykonując pętlę szeregowo.

[Top]

Ekspresowa równoległość na najwyższym możliwym poziomie

W przypadku równoległości kodu tylko na niskim poziomie można wprowadzić konstrukcję sprzężenia rozwidlenia, która nie jest skalowana w miarę wzrostu liczby procesorów. Konstrukcja sprzężenia rozwidlenia to konstrukcja, w której jedno zadanie dzieli pracę na mniejsze podzadania równoległe i czeka na zakończenie tych podzadań. Każda podzadania może rekursywnie podzielić się na dodatkowe podzadania.

Chociaż model łączenia rozwidlenia może być przydatny do rozwiązywania różnych problemów, istnieją sytuacje, w których obciążenie synchronizacji może zmniejszyć skalowalność. Rozważmy na przykład następujący kod seryjny, który przetwarza dane obrazu.

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

Ponieważ każda iteracja pętli jest niezależna, można zrównać większość pracy, jak pokazano w poniższym przykładzie. W tym przykładzie użyto współbieżności::p arallel_for algorytmu w celu zrównania pętli zewnętrznej.

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

Poniższy przykład ilustruje konstrukcję sprzężenia rozwidlenia przez wywołanie ProcessImage funkcji w pętli. Każde wywołanie polecenia ProcessImage nie zostanie zwrócone, dopóki nie zakończy się każda podzadanie.

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

Jeśli każda iteracja pętli równoległej wykonuje prawie nie pracę lub praca wykonywana przez pętlę równoległą jest niezrównoważony, oznacza to, że niektóre iteracji pętli trwa dłużej niż inne, obciążenie związane z planowaniem, które jest wymagane do częstego rozwidlenia i pracy sprzężenia może przewyższać korzyści z równoległego wykonywania. To obciążenie zwiększa się wraz ze wzrostem liczby procesorów.

Aby zmniejszyć ilość obciążeń związanych z planowaniem w tym przykładzie, można zrównać pętle zewnętrzne przed zrównanie pętli wewnętrznych lub użyć innej konstrukcji równoległej, takiej jak potokowanie. Poniższy przykład modyfikuje ProcessImages funkcję w celu użycia współbieżności::p arallel_for_each algorytmu w celu zrównania pętli zewnętrznej.

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

Podobny przykład, który używa potoku do równoległego przetwarzania obrazów, zobacz Przewodnik: tworzenie sieci przetwarzania obrazów.

[Top]

Rozwiązywanie problemów z podziałem i podbijanie przy użyciu parallel_invoke

Problem dzielenia i podbijania jest formą konstrukcji sprzężenia rozwidlenia, która używa rekursji do dzielenia zadania na podzadania. Oprócz klas współbieżności::task_group i współbieżności::structured_task_group można również użyć algorytmu concurrency::p arallel_invoke , aby rozwiązać problemy z podziałem i podbojem. Algorytm parallel_invoke ma bardziej zwięzłą składnię niż obiekty grupy zadań i jest przydatny, gdy masz stałą liczbę zadań równoległych.

Poniższy przykład ilustruje użycie algorytmu parallel_invoke do zaimplementowania algorytmu sortowania bitoicznego.

// 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 zmniejszyć obciążenie, parallel_invoke algorytm wykonuje ostatnią serię zadań w kontekście wywołującym.

Aby uzyskać pełną wersję tego przykładu, zobacz How to: Use parallel_invoke to Write a Parallel Sort Routine (Instrukcje: używanie parallel_invoke do zapisywania równoległej procedury sortowania). Aby uzyskać więcej informacji na temat algorytmu parallel_invoke , zobacz Parallel Algorithms (Algorytmy równoległe).

[Top]

Używanie obsługi anulowania lub wyjątków w celu przerwania pętli równoległej

PPL zapewnia dwa sposoby anulowania równoległej pracy wykonywanej przez grupę zadań lub algorytm równoległy. Jednym ze sposobów jest użycie mechanizmu anulowania udostępnianego przez klasy współbieżności::task_group i współbieżności::structured_task_group . Innym sposobem jest zgłoszenie wyjątku w treści funkcji pracy zadania. Mechanizm anulowania jest bardziej wydajny niż obsługa wyjątków podczas anulowania drzewa równoległej pracy. Równoległe drzewo robocze to grupa powiązanych grup zadań, w których niektóre grupy zadań zawierają inne grupy zadań. Mechanizm anulowania anuluje grupę zadań i jej podrzędne grupy zadań w sposób od góry do dołu. Z drugiej strony obsługa wyjątków działa w sposób dolny i musi anulować niezależnie każdą podrzędną grupę zadań, ponieważ wyjątek jest propagowany w górę.

Podczas bezpośredniej pracy z obiektem grupy zadań użyj współbieżności::task_group::cancel lub współbieżności::structured_task_group::cancel , aby anulować pracę należącą do tej grupy zadań. Aby anulować algorytm równoległy, na przykład , parallel_forutwórz nadrzędną grupę zadań i anuluj ją. Rozważmy na przykład następującą funkcję , parallel_find_anyktóra wyszukuje wartość w tablicy równolegle.

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

Ponieważ algorytmy równoległe używają grup zadań, gdy jedna z iteracji równoległych anuluje nadrzędną grupę zadań, całkowite zadanie zostanie anulowane. Aby uzyskać pełną wersję tego przykładu, zobacz Instrukcje: używanie anulowania do przerwania z pętli równoległej.

Chociaż obsługa wyjątków jest mniej wydajnym sposobem anulowania równoległej pracy niż mechanizm anulowania, istnieją przypadki, w których odpowiednie jest obsługiwanie wyjątków. Na przykład następująca metoda rekursywnie for_allwykonuje funkcję pracy w każdym węźle tree struktury. W tym przykładzie element _children członkowski danych jest listą std::list zawierającą tree obiekty.

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

Obiekt wywołujący tree::for_all metody może zgłosić wyjątek, jeśli nie wymaga wywołania funkcji pracy dla każdego elementu drzewa. Poniższy przykład przedstawia search_for_value funkcję, która wyszukuje wartość w podanym tree obiekcie. Funkcja search_for_value używa funkcji pracy, która zgłasza wyjątek, gdy bieżący element drzewa jest zgodny z podaną wartością. Funkcja search_for_value używa try-catch bloku do przechwytywania wyjątku i drukowania wyniku w konsoli.

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

Aby uzyskać pełną wersję tego przykładu, zobacz How to: Use Exception Handling to Break from a Parallel Loop (Instrukcje: używanie obsługi wyjątków w celu przerwania pętli równoległej).

Aby uzyskać więcej ogólnych informacji na temat mechanizmów anulowania i obsługi wyjątków, które są udostępniane przez PPL, zobacz Anulowanie w PPL i Obsługa wyjątków.

[Top]

Informacje o tym, jak anulowanie i obsługa wyjątków wpływają na niszczenie obiektów

W drzewie równoległej pracy anulowane zadanie uniemożliwia uruchamianie zadań podrzędnych. Może to spowodować problemy, jeśli jedno z zadań podrzędnych wykonuje operację, która jest ważna dla aplikacji, na przykład zwalniając zasób. Ponadto anulowanie zadania może spowodować wyjątek propagacji za pomocą destruktora obiektu i spowodować niezdefiniowane zachowanie w aplikacji.

W poniższym przykładzie Resource klasa opisuje zasób, a Container klasa opisuje kontener, który zawiera zasoby. W destruktorze Container klasa wywołuje metodę cleanup na dwóch jej Resource składowych równolegle, a następnie wywołuje metodę cleanup na trzecim Resource elemencie członkowskim.

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

Mimo że ten wzorzec nie ma własnych problemów, rozważ następujący kod, który równolegle uruchamia dwa zadania. Pierwsze zadanie tworzy Container obiekt, a drugie zadanie anuluje ogólne zadanie. Na ilustracji w przykładzie użyto dwóch obiektów współbieżności::event , aby upewnić się, że anulowanie występuje po utworzeniu Container obiektu i że Container obiekt zostanie zniszczony po zakończeniu operacji anulowania.

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

Ten przykład generuje następujące wyniki:

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

Ten przykład kodu zawiera następujące problemy, które mogą powodować, że zachowuje się inaczej niż oczekiwano:

  • Anulowanie zadania nadrzędnego powoduje również anulowanie zadania podrzędnego, wywołanie współbieżności::p arallel_invoke. W związku z tym te dwa zasoby nie są zwalniane.

  • Anulowanie zadania nadrzędnego powoduje, że zadanie podrzędne zgłasza wyjątek wewnętrzny. Container Ponieważ destruktor nie obsługuje tego wyjątku, wyjątek jest propagowany w górę, a trzeci zasób nie jest zwalniany.

  • Wyjątek zgłaszany przez zadanie podrzędne jest propagowany za pośrednictwem destruktora Container . Wyrzucenie z destruktora powoduje, że aplikacja jest w stanie niezdefiniowanym.

Zalecamy, aby nie wykonywać krytycznych operacji, takich jak zwalnianie zasobów, w zadaniach, chyba że można zagwarantować, że te zadania nie zostaną anulowane. Zalecamy również, aby nie używać funkcji środowiska uruchomieniowego, które mogą zgłaszać destruktor typów.

[Top]

Nie blokuj wielokrotnie w pętli równoległej

Pętla równoległa, taka jak concurrency::p arallel_for lub concurrency::p arallel_for_each zdominowana przez operacje blokujące, może spowodować utworzenie przez środowisko uruchomieniowe wielu wątków w krótkim czasie.

Środowisko uruchomieniowe współbieżności wykonuje dodatkową pracę, gdy zadanie zostanie zakończone lub spółdzielnie blokuje lub zwraca wydajność. Gdy jedna iteracja pętli równoległej blokuje, środowisko uruchomieniowe może rozpocząć inną iterację. Jeśli nie ma dostępnych wątków bezczynnych, środowisko uruchomieniowe tworzy nowy wątek.

Gdy treść pętli równoległej od czasu do czasu blokuje, ten mechanizm pomaga zmaksymalizować ogólną przepływność zadań. Jeśli jednak wiele iteracji blokuje, środowisko uruchomieniowe może utworzyć wiele wątków w celu uruchomienia dodatkowej pracy. Może to prowadzić do niskiej ilości pamięci lub słabego wykorzystania zasobów sprzętowych.

Rozważmy następujący przykład, który wywołuje funkcję concurrency::send w każdej iteracji parallel_for pętli. Ponieważ send blokuje wspólnie, środowisko uruchomieniowe tworzy nowy wątek do uruchamiania dodatkowej pracy za każdym razem, gdy send jest wywoływany.

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

Zalecamy refaktoryzację kodu, aby uniknąć tego wzorca. W tym przykładzie można uniknąć tworzenia dodatkowych wątków, wywołując w send pętli szeregowej for .

[Top]

Nie wykonuj operacji blokujących podczas anulowania równoległej pracy

Jeśli to możliwe, nie wykonuj operacji blokowania przed wywołaniem metody concurrency::task_group::cancel lub concurrency::structured_task_group::cancel , aby anulować pracę równoległą.

Gdy zadanie wykonuje operację blokującą spółdzielnię, środowisko uruchomieniowe może wykonać inną pracę, podczas gdy pierwsze zadanie czeka na dane. Środowisko uruchomieniowe ponownie uruchamia zadanie oczekujące podczas odblokowywania. Środowisko uruchomieniowe zwykle ponownie wykonuje zadania, które zostały ostatnio odblokowane przed ponownym harmonogramem zadań, które były mniej ostatnio odblokowane. W związku z tym środowisko uruchomieniowe może zaplanować niepotrzebną pracę podczas operacji blokowania, co prowadzi do zmniejszenia wydajności. W związku z tym po wykonaniu operacji blokującej przed anulowaniem równoległej pracy operacja blokowania może opóźnić wywołanie metody cancel. Powoduje to wykonywanie niepotrzebnych zadań.

Rozważmy poniższy przykład, który definiuje parallel_find_answer funkcję, która wyszukuje element podanej tablicy, który spełnia podaną funkcję predykatu. Gdy funkcja predykat zwraca truewartość , funkcja równoległej pracy tworzy Answer obiekt i anuluje ogólne zadanie.

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

Operator new wykonuje alokację sterty, która może blokować. Środowisko uruchomieniowe wykonuje inną pracę tylko wtedy, gdy zadanie wykonuje wywołanie blokujące spółdzielnię, takie jak wywołanie współbieżności::critical_section::lock.

W poniższym przykładzie pokazano, jak zapobiec niepotrzebnej pracy, a tym samym zwiększyć wydajność. W tym przykładzie grupa zadań zostanie anulowana, zanim przydzieli magazyn dla Answer obiektu.

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

[Top]

Nie zapisuj danych udostępnionych w pętli równoległej

Środowisko uruchomieniowe współbieżności udostępnia kilka struktur danych, na przykład współbieżność::critical_section, które synchronizują współbieżny dostęp do udostępnionych danych. Te struktury danych są przydatne w wielu przypadkach, na przykład wtedy, gdy wiele zadań rzadko wymaga dostępu współdzielonego do zasobu.

Rozważmy następujący przykład, który używa algorytmu concurrency::p arallel_for_each i critical_section obiektu do obliczenia liczby liczb pierwszych w obiekcie std::array . Ten przykład nie jest skalowany, ponieważ każdy wątek musi czekać, aby uzyskać dostęp do zmiennej udostępnionej 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();
});

Ten przykład może również prowadzić do niskiej wydajności, ponieważ częsta operacja blokowania skutecznie serializuje pętlę. Ponadto, gdy obiekt środowiska uruchomieniowego współbieżności wykonuje operację blokującą, harmonogram może utworzyć dodatkowy wątek, aby wykonać inną pracę, podczas gdy pierwszy wątek czeka na dane. Jeśli środowisko uruchomieniowe tworzy wiele wątków, ponieważ wiele zadań oczekuje na udostępnione dane, aplikacja może działać słabo lub wprowadzić stan niskiego zasobu.

PPL definiuje klasę współbieżności::łączenie , która pomaga wyeliminować stan udostępniony, zapewniając dostęp do udostępnionych zasobów w sposób wolny od blokady. Klasa combinable udostępnia magazyn lokalny wątków, który umożliwia wykonywanie precyzyjnych obliczeń, a następnie scalanie tych obliczeń w końcowy wynik. Obiekt można traktować combinable jako zmienną redukcji.

Poniższy przykład modyfikuje poprzedni, używając combinable obiektu zamiast critical_section obiektu w celu obliczenia sumy. Ten przykład jest skalowany, ponieważ każdy wątek przechowuje własną lokalną kopię sumy. W tym przykładzie użyto metody concurrency::combineable::combine , aby scalić lokalne obliczenia w końcowy wynik.

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

Aby uzyskać pełną wersję tego przykładu, zobacz How to: Use combinable to Improve Performance (Instrukcje: używanie możliwości łączenia w celu zwiększenia wydajności). Aby uzyskać więcej informacji na temat combinable klasy, zobacz Parallel Containers and Objects (Kontenery równoległe i obiekty).

[Top]

Jeśli to możliwe, unikaj udostępniania fałszywych

Współużytkowanie fałszywe występuje, gdy wiele współbieżnych zadań uruchomionych na osobnych procesorach zapisuje w zmiennych znajdujących się w tym samym wierszu pamięci podręcznej. Gdy jedno zadanie zapisuje w jednej ze zmiennych, wiersz pamięci podręcznej dla obu zmiennych jest unieważniany. Każdy procesor musi ponownie załadować wiersz pamięci podręcznej za każdym razem, gdy wiersz pamięci podręcznej jest unieważniony. W związku z tym udostępnianie false może spowodować zmniejszenie wydajności aplikacji.

W poniższym przykładzie podstawowym przedstawiono dwa współbieżne zadania, z których każda zwiększa współdzieloną zmienną licznika.

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

Aby wyeliminować udostępnianie danych między dwoma zadaniami, możesz zmodyfikować przykład, aby użyć dwóch zmiennych liczników. W tym przykładzie obliczana jest końcowa wartość licznika po zakończeniu zadań. Jednak w tym przykładzie pokazano fałszywe współużytkowanie, ponieważ zmienne count1 i count2 prawdopodobnie będą znajdować się w tym samym wierszu pamięci podręcznej.

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;

Jednym ze sposobów wyeliminowania fałszywego udostępniania jest upewnienie się, że zmienne licznika znajdują się w osobnych wierszach pamięci podręcznej. Poniższy przykład wyrównuje zmienne count1 i count2 granice 64 bajtów.

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

W tym przykładzie przyjęto założenie, że rozmiar pamięci podręcznej wynosi 64 lub mniej bajtów.

Zalecamy użycie klasy concurrency::combinable , gdy musisz udostępniać dane między zadaniami. Klasa combinable tworzy zmienne wątkowo-lokalne w taki sposób, że udostępnianie false jest mniej prawdopodobne. Aby uzyskać więcej informacji na temat combinable klasy, zobacz Parallel Containers and Objects (Kontenery równoległe i obiekty).

[Top]

Upewnij się, że zmienne są prawidłowe przez cały okres istnienia zadania

Po podaniu wyrażenia lambda do grupy zadań lub algorytmu równoległego klauzula przechwytywania określa, czy treść wyrażenia lambda uzyskuje dostęp do zmiennych w otaczającym zakresie według wartości lub przez odwołanie. Po przekazaniu zmiennych do wyrażenia lambda przez odwołanie należy zagwarantować, że okres istnienia tej zmiennej będzie się powtarzać do momentu zakończenia zadania.

Rozważmy poniższy przykład, który definiuje klasę objectperform_action i funkcję. Funkcja perform_action tworzy zmienną object i wykonuje pewną akcję na tej zmiennej asynchronicznie. Ponieważ zadanie nie ma gwarancji zakończenia przed perform_action zwróceniem funkcji, program ulegnie awarii lub będzie wykazywał nieokreślone zachowanie, jeśli object zmienna zostanie zniszczona po uruchomieniu zadania.

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

W zależności od wymagań aplikacji można użyć jednej z następujących technik, aby zagwarantować, że zmienne pozostaną prawidłowe przez cały okres istnienia każdego zadania.

Poniższy przykład przekazuje zmienną object według wartości do zadania. W związku z tym zadanie działa na własnej kopii zmiennej.

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

Ponieważ zmienna object jest przekazywana przez wartość, wszelkie zmiany stanu występujące w tej zmiennej nie są wyświetlane w oryginalnej kopii.

W poniższym przykładzie użyto metody concurrency::task_group::wait , aby upewnić się, że zadanie zostanie zakończone przed zwróceniem perform_action funkcji.

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

Ponieważ zadanie kończy się teraz przed zwróceniem funkcji, perform_action funkcja nie zachowuje się już asynchronicznie.

W poniższym przykładzie perform_action funkcja modyfikuje odwołanie do zmiennej object . Obiekt wywołujący musi zagwarantować, że okres istnienia zmiennej object jest prawidłowy do momentu zakończenia zadania.

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

Możesz również użyć wskaźnika do kontrolowania okresu istnienia obiektu przekazywanego do grupy zadań lub algorytmu równoległego.

Aby uzyskać więcej informacji na temat wyrażeń lambda, zobacz Wyrażenia lambda.

[Top]

Zobacz też

Środowisko uruchomieniowe współbieżności — najlepsze praktyki
Biblioteka równoległych wzorców (PLL)
Równoległe kontenery oraz obiekty
Algorytmy równoległe
Anulowanie w PPL
Obsługa wyjątków
Przewodnik: tworzenie sieci przetwarzania obrazów
Instrukcje: używanie parallel_invoke do napisania procedury sortowania równoległego
Instrukcje: używanie anulowania, aby przerwać pętlę równoległą
Instrukcje: korzystanie z wyników połączonych do poprawiania wydajności
Biblioteka agentów asynchronicznych — najlepsze praktyki
Środowisko uruchomieniowe współbieżności — najlepsze praktyki ogólne