Udostępnij za pośrednictwem


Najlepszych praktyk w bibliotece desenie równoległe

W tym dokumencie opisano jak najlepiej Aby efektywnie wykorzystać równoległych Biblioteka wzorców (PPL).PPL zapewnia kontenerów ogólnego przeznaczenia, obiekty i algorytmów do wykonywania drobnoziarnistych równoległości prostych.

Aby uzyskać więcej informacji na temat PPL, zobacz Biblioteka desenie równoległe (PPL).

Sekcje

Ten dokument zawiera następujące sekcje:

  • Nie zrównoleglenia małych organów pętli

  • Express równoległości na najwyższym możliwym poziomie

  • Użyj parallel_invoke do Rozwiązywanie problemów z dzielenia i zdobyć

  • Użyj anulowania lub obsługi wyjątków do podziału z równoległych pętli

  • Zrozumieć, jak unieważnienia umowy lub obsługa wyjątków wpływają na zniszczenie obiektu

  • Nie należy blokować wielokrotnie w pętli równoległego

  • Nie wykonuj blokowanie operacji, po anulowaniu równoległą pracę

  • Nie pisz do udostępnionych danych w pętli równoległego

  • Jeśli to możliwe, uniknięcie fałszywych udostępnianie

  • Upewnij się, że zmienne są ważne przez cały okres istnienia zadania

Nie zrównoleglenia małych organów pętli

Zrównoleglenia organów stosunkowo małą pętlę może spowodować skojarzone planowanie ma dodatkowe obciążenie przewyższają zalety równoległe przetwarzanie.Rozważmy następujący przykład, który dodaje każdej pary 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 pracą dla każdej iteracji pętli równoległego jest zbyt mały, aby korzystać z napowietrznej przetwarzania równoległego.Wykonując więcej pracy w treści pętli lub wykonując pętli pojedynczo, może zwiększyć wydajność tej pętli.

Top

Express równoległości na najwyższym możliwym poziomie

Gdy zrównoleglenia się kodu tylko na niskim poziomie, może wprowadzić konstrukcja rozwidlenie sprzężenia, która nie jest skalowany jako liczba procesorów wzrasta.A rozwidlenia join konstruują jest konstrukcji, gdzie jedno zadanie dzieli swoją pracę na mniejsze równoległych podzadań i czeka na te podzadania do końca.Każdego z nich można rekursywnie divide na dodatkowe podzadania.

Chociaż model rozwidlenie sprzężenia może być bardzo przydatny w rozwiązywaniu różnych problemów, są sytuacje, gdzie kosztów synchronizacji może zmniejszyć skalowalność.Na przykład rozważmy 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żdej iteracji pętli jest niezależna, można zrównoleglenia znaczną część pracy, jak pokazano w poniższym przykładzie.W poniższym przykładzie użyto concurrency::parallel_for algorytm zrównoleglają zewnętrznej pętli.

// 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 konstrukcja rozwidlenie sprzężenia, wywołując ProcessImage funkcja w pętli.Każde wywołanie ProcessImage nie zwraca dopóki nie zakończy się każdego z nich.

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

W przypadku każdej iteracji pętli równoległego, albo wykonuje prawie żadna praca lub pracy, wykonywaną przez równoległe pętli nierówne, oznacza to, niektórych powtórzeń pętli trwać dłużej niż inne, planowanie napowietrznej jest wymagane do talerza często i łączyć pracy można przewyższają korzyści dla przetwarzania równoległego.Zwiększa to obciążenie jako liczba procesorów wzrasta.

Aby zmniejszyć ilość planowania obciążenie, w tym przykładzie, można zrównoleglenia zewnętrznych pętli, zanim zrównoleglenia wewnętrznej pętli lub użyj innej konstrukcji równoległego, takich jak rurociąg.Poniższy przykład modyfikuje ProcessImages funkcję do użycia concurrency::parallel_for_each algorytm zrównoleglają zewnętrznej pętli.

// 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 używa rurociągu przeprowadzać równolegle do przetwarzania obrazu, zobacz Instruktaż: Tworzenie sieci przetwarzania obrazu.

Top

Użyj parallel_invoke do Rozwiązywanie problemów z dzielenia i zdobyć

A divide i zdobyć problem jest formą konstruują rozwidlenie sprzężenia, który używa rekursji do dzielą zadania na podzadania.W uzupełnieniu do concurrency::task_group i concurrency::structured_task_group klas, można również użyć concurrency::parallel_invoke algorytm do rozwiązania problemów divide i zdobyć.parallel_invoke Algorytm ma bardziej zwięzłe składnię niż obiekty grupy zadań i jest przydatne, gdy masz stałą liczbę zadań równoległych.

Poniższy przykład ilustruje użycie parallel_invoke algorytm zrealizować bitonic sortowanie.

// 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 ją zmniejszyć, parallel_invoke algorytm wykonuje ostatniego serię zadań w kontekście wywołującego.

Aby uzyskać pełną wersję w tym przykładzie, zobacz Jak: za pomocą parallel_invoke zapisu równoległych rutynowych sortowania.Aby uzyskać więcej informacji o parallel_invoke algorytmu, zobacz Algorytmy równoległe.

Top

Użyj anulowania lub obsługi wyjątków do podziału z równoległych pętli

PPL oferuje dwa sposoby, aby anulować równoległą pracę, wykonywaną przez grupy zadań lub algorytm równoległy.Jednym ze sposobów jest użycie mechanizmu anulowania, dostarczonego przez concurrency::task_group i concurrency::structured_task_group klas.Innym sposobem jest zgłaszanie wyjątku w treści funkcji pracy zadania.Mechanizm odwołania jest bardziej efektywne niż poprzez wyjątki w anulowanie drzewo równoległą pracę.A równoległą pracę drzewa jest grupą grup powiązanych zadań, w których niektóre grupy zadań zawierają inne grupy zadań.Mechanizm anulowania anuluje grupę zadań i jego grup zadań podrzędnych w sposób góra dół.I odwrotnie obsługa wyjątków działa na zasadzie dół góra i musisz anulować poszczególnych grupach zadań podrzędnych niezależnie jako wyjątek propaguje w górę.

Podczas pracy bezpośrednio z obiektu grupy zadań, użyj concurrency::task_group::cancel lub concurrency::structured_task_group::cancel metod, aby anulować pracę, która należy do tej grupy zadań.Aby anulować algorytm równoległy, na przykład, parallel_for, należy utworzyć grupę zadania nadrzędnego i anulowanie tej grupy zadań.Rozważmy na przykład poniższa funkcja parallel_find_any, który wyszukuje wartość w tablicy jednocześnie.

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

Algorytmy równoległe korzystać grupy zadań, kiedy jeden z równoległych iteracji anuluje nadrzędna grupa zadań, dlatego podstawowym zadaniem jest anulowane.Aby uzyskać pełną wersję w tym przykładzie, zobacz Jak: umożliwia anulowanie podziału z pętli równoległe.

Chociaż obsługa wyjątków jest mniej wydajnym sposobem anulować równoległą pracę niż mechanizm odwołania, są przypadki, gdy obsługa wyjątków jest odpowiedni.Na przykład, następujące metody, for_all, rekursywnie pełni funkcję pracy na każdym węźle tree struktury.W tym przykładzie _children element członkowski danych jest std::list zawiera tree obiektów.

// 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 metoda może zgłosić wyjątek, jeśli nie wymaga funkcji pracy do wywołania na każdy element drzewa.W poniższym przykładzie pokazano search_for_value funkcję, która wyszukuje wartość w dołączonym tree obiektu.search_for_value Funkcja używa funkcji pracy, która zgłasza wyjątek, gdy podana wartość pasuje do bieżącego elementu drzewa.search_for_value Funkcja używa try-catch bloku na przechwytywanie wyjątku i wydrukuje wyniki do 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ę w tym przykładzie, zobacz Jak: Użyj obsługi wyjątków do podziału z pętli równoległe.

Aby uzyskać więcej informacji o anulowaniu i mechanizmów obsługi wyjątków, które zostały udostępnione przez PPL, zobacz Anulowanie w PPL i Obsługa wyjątków w Runtime współbieżności.

Top

Zrozumieć, jak unieważnienia umowy lub obsługa wyjątków wpływają na zniszczenie obiektu

W drzewie równoległą pracę zadanie, które zostało anulowane zapobiega zadań podrzędnych z systemem.Może to powodować problemy, jeśli jedno z zadań podrzędnych wykonuje operację, która jest ważne dla aplikacji, takich jak zwalniania zasób.Ponadto anulowanie zadania może spowodować wyjątek Propagacja destruktor obiektu i spowodować, że zachowanie niezdefiniowane w aplikacji.

W poniższym przykładzie Resource klasy opisuje zasób i Container klasa opisuje kontener, który zawiera zasoby.W jego destruktora Container klasy wywołania cleanup metody na dwóch jego Resource równolegle, a następnie wywołań cleanup metody na jej trzeciej Resource Członkowskich.

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

Chociaż tego wzorca nie ma problemów z własnej, należy rozważyć następujący kod, który uruchamia dwa zadania równolegle.Tworzy pierwszego zadania Container obiektu i drugie zadanie anuluje ogólne zadanie.Na ilustracji w przykładzie użyto dwóch concurrency::event obiektów, aby upewnić się, że anulowanie pojawia się po Container obiekt jest tworzony i że Container niszczony jest obiekt, po wystąpieniu 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:

  
  

W tym przykładzie kodu zawiera następujące problemy, które mogą spowodować, że zachowują się inaczej niż oczekujesz, że:

  • Anulowanie zadania nadrzędnego powoduje zadania podrzędne, wywołanie concurrency::parallel_invoke, również zostać anulowane.W związku z tym te dwa zasoby nie są zwalniane.

  • Anulowanie zadania nadrzędnego powoduje zadania podrzędne Zgłoś wyjątek wewnętrzny.Ponieważ Container destruktora obsługuje ten wyjątek, wyjątek są propagowane w górę i trzeciego zasobu nie jest zwalniane.

  • Wyjątek, który jest generowany przez zadanie podrzędne rozchodzi się poprzez Container destruktor.Wyrzucanie z destruktora stawia aplikacji w stanie niezdefiniowany.

Zaleca się, że nie zostaną wykonane operacje krytyczne, na przykład uwalniając zasoby w widoku zadania, o ile nie może zagwarantować, że te zadania nie zostaną anulowane.Zaleca się również, że nie używasz funkcji środowiska wykonawczego, które można rzucić w destruktora swój typ.

Top

Nie należy blokować wielokrotnie w pętli równoległego

Równoległe pętli, takich jak concurrency::parallel_for lub concurrency::parallel_for_each który jest zdominowany przez blokowanie operacji może spowodować runtime utworzyć wiele wątków przez krótki czas.

Runtime współbieżności wykonuje dodatkową pracę, gdy zadanie kończy się lub blokuje wspólnie lub plonów.Podczas jednej równoległy pętli bloki iteracji, środowiska wykonawczego może zacząć innego iteracji.Jeśli nie ma żadnych dostępne wątki bezczynne, środowiska wykonawczego tworzy nowy wątek.

Gdy organ równolegle od czasu do czasu pętli bloki, ten mechanizm pomaga, zwiększenie ogólnej wydajności zadań.Jednak gdy zablokować dużo iteracji, środowiska wykonawczego może utworzyć wiele wątków, aby uruchomić dodatkową pracę.Może to prowadzić do ilość pamięci jest mała lub złego wykorzystania zasobów sprzętowych.

Rozważmy następujący przykład, który wywołuje concurrency::send funkcji w każdej iteracji parallel_for pętli.Ponieważ send wspólnie, blokuje środowiska wykonawczego tworzy nowy wątek do uruchomienia dodatkowej pracy za każdym razem send nosi nazwę.

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

Zaleca się, aby poprawiać pewne swój kod, aby uniknąć tego wzorca.W tym przykładzie, można uniknąć tworzenia dodatkowych wątków, wywołując send w portu szeregowego for pętli.

Top

Nie wykonuj blokowanie operacji, po anulowaniu równoległą pracę

Jeśli to możliwe, nie należy wykonywać operacji blokowania zanim zadzwonisz do concurrency::task_group::cancel lub concurrency::structured_task_group::cancel metodę, aby anulować równoległą pracę.

Kiedy zadania wykonuje spółdzielni blokuje operację, aparat plików wykonywalnych podczas można wykonywać innych prac pierwsze zadanie czeka na dane.Aparat plików wykonywalnych zmienia harmonogram zadań oczekujących, gdy go odblokowuje.Środowisko wykonawcze zazwyczaj zmienia harmonogram zadań, które były bardziej niedawno odblokowane, zanim ponownie zaplanuje wykonanie zadań, które były mniej niedawno odblokowane.W związku z tym środowiska wykonawczego może zaplanować niepotrzebnej pracy podczas operacji blokowania, co prowadzi do obniżonej wydajności.W związku z tym wykonywanie operacja blokująca zanim anulujesz równoległą pracę, operacja blokująca można opóźnić wywołanie cancel.Powoduje to, że inne zadania do wykonania niepotrzebnej pracy.

Rozważmy następujący przykład, który definiuje parallel_find_answer funkcję, która wyszukuje element dostarczonej tablicy, który spełnia funkcję predykatu pod warunkiem.Kiedy funkcja predykatu zwraca true, równoległych funkcja pracy tworzy Answer object 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;
}

new Operator wykonuje alokację sterty, który może spowodować zablokowanie.Środowisko wykonawcze wykonuje inne prace tylko wtedy, gdy wykonuje zadanie spółdzielni blokowanie połączenia, takie jak wywołanie concurrency::critical_section::lock.

Jak zapobiec niepotrzebnej pracy i tym samym zwiększyć wydajność można znaleźć w poniższym przykładzie.W tym przykładzie spowoduje anulowanie grupy zadań, zanim go przydziela pamięci masowej 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 pisz do udostępnionych danych w pętli równoległego

Runtime współbieżności zawiera kilka struktur danych, na przykład, concurrency::critical_section, który zsynchronizować równoczesny dostęp do udostępnionych danych.Tych struktur danych są przydatne w wielu przypadkach, na przykład, gdy więcej niż jedno zadanie rzadko wymagają współdzielony dostęp do zasobu.

Rozważmy następujący przykład, który używa concurrency::parallel_for_each algorytmu i critical_section obiekt, aby obliczyć liczbę elementów w zachodniej std::array obiektu.W tym przykładzie nie jest skalowany, ponieważ każdy wątek musi czekać uzyskać dostęp do udostępnionych zmiennej 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();
});

W tym przykładzie może również prowadzić do niskiej wydajności, ponieważ częste operacji blokowania skutecznie serializes pętli.Dodatkowo gdy obiekt Runtime współbieżności wykonuje operacja blokująca, harmonogram utworzyć dodatkowy wątek do wykonywania innych prac, gdy pierwszy wątek oczekuje na dane.Jeśli środowisko wykonawcze tworzy wiele wątków, ponieważ wiele zadań oczekują na udostępnionych danych, aplikacja może działać niezadowalającą lub przejście w stan niskiego (zasób).

Definiuje ten PPL concurrency::combinable klasy, która pomaga wyeliminować stan udostępnionego poprzez zapewnienie dostępu do zasobów udostępnionych w sposób wolny blokady.combinable Klasa daje pamięci lokalnej wątku, który umożliwia wykonywanie obliczeń szczegółowymi zasadami i następnie scalić tych obliczeń w wyniku końcowego.Możesz myśleć o combinable obiektu jako zmienna redukcji.

Poniższy przykład modyfikuje poprzedniego za pomocą combinable obiekt zamiast critical_section obiekt, aby obliczyć sumę.W tym przykładzie wag elektronicznych, ponieważ każdy wątek posiada własne kopie sumy.W poniższym przykładzie użyto concurrency::combinable::combine metoda scalenie lokalnych obliczeń w wyniku końcowego.

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ę w tym przykładzie, zobacz Jak: Użyj łączonymi do poprawy wydajności.Aby uzyskać więcej informacji o combinable klasy, zobacz Równoległe kontenerów i obiektów.

Top

Jeśli to możliwe, uniknięcie fałszywych udostępnianie

Udostępnianie fałszywe występuje, gdy wiele równoczesnych zadań, uruchomionych na oddzielnych procesorów zapisu zmiennych, które znajdują się na tym samym wierszu pamięci podręcznej.Gdy jedno zadanie zapisuje się do jednej ze zmiennych, zostaje unieważniony wiersz pamięci podręcznej dla obu zmiennych.Każdy procesor musi ponownie wczytać wiersza pamięci podręcznej każdym razem, gdy unieważniono wiersza pamięci podręcznej.W związku z tym udostępnianie FAŁSZ może spowodować obniżenie wydajności w aplikacji.

Prosty przykład pokazuje dwóch równoczesnych czynności każdy przyrost wartości zmiennej counter udostępnionego.

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ć, wymiany danych między dwoma zadaniami, można zmodyfikować przykład, aby używać dwie zmienne liczników.Ten przykład oblicza wartości końcowej licznika po zakończenia zadania.Jednak ten przykład ilustruje, udostępnianie wartość false, ponieważ zmienne count1 i count2 mogą 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łszywe udostępniania jest upewnienie się, że zmienne liczników są w pamięci podręcznej w oddzielnych wierszach.Poniższy przykład wyrównuje zmienne count1 i count2 na 64-bajtowych granic.

__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 przykładzie założono, że rozmiar pamięci podręcznej wynosi 64 lub mniejszej liczby bajtów.

Firma Microsoft zaleca użycie concurrency::combinable klasy, gdy trzeba udostępnić danych między zadaniami.combinable Klasy tworzy zmienne lokalna dla wątku w taki sposób, że FAŁSZ udostępniania jest mniej prawdopodobne.Aby uzyskać więcej informacji o combinable klasy, zobacz Równoległe kontenerów i obiektów.

Top

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

W przypadku dostarczania wyrażenia lambda do grupy zadań lub algorytm równoległy, klauzula przechwytywania określa, czy treści Wyrażenie lambda uzyskuje dostęp do zmiennych w zasięgu, przez wartość lub przez odwołanie.Jeśli zmienne do wyrażenia lambda przez odniesienie, muszą gwarantować istnienia tej zmiennej obowiązuje, dopóki nie zakończy się zadanie.

Rozważmy następujący przykład, który definiuje object klasy i perform_action funkcji.perform_action Funkcja tworzy object zmienna i wykonuje pewne działania na tej zmiennej asynchronicznie.Ponieważ zadanie nie jest gwarantowana do końca przed perform_action funkcja zwróci wartość, będzie ulec awarii lub może zawierać zachowanie nieokreślona, jeśli object zmienna jest niszczony, kiedy zadanie jest uruchomione.

// 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 do zagwarantowania, że zmienne pozostają ważne przez cały okres istnienia każdego zadania.

Poniższy przykład przekazuje object zmiennej przez wartość z zadaniem.W związku z tym zadanie działa na własną kopię 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ż object zmienna jest przekazywany przez wartość, wszelkie jego stan zmiany do tej zmiennej nie są wyświetlane w oryginalnej kopii.

W poniższym przykładzie użyto concurrency::task_group::wait metodę, aby upewnić się, że zadanie kończy się przed perform_action , funkcja zwraca.

// 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 teraz dopiero po całkowitym wykonaniu funkcja zwraca, perform_action funkcja nie jest już działa asynchronicznie.

Poniższy przykład modyfikuje perform_action przez funkcję odniesienie do object zmiennej.Obiekt wywołujący musi zagwarantować, że okres istnienia object zmienna jest nieprawidłowa, dopóki nie zakończy się zadanie.

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

Umożliwia także wskaźnik do kontrolowania istnienia obiektu, który jest przekazywany do grupy zadań lub algorytm równoległy.

Aby uzyskać więcej informacji na temat wyrażeń lambda zobacz Lambda wyrażenia w języku C++.

Top

Zobacz też

Zadania

Instruktaż: Tworzenie sieci przetwarzania obrazu

Jak: za pomocą parallel_invoke zapisu równoległych rutynowych sortowania

Jak: umożliwia anulowanie podziału z pętli równoległe

Jak: Użyj łączonymi do poprawy wydajności

Koncepcje

Biblioteka desenie równoległe (PPL)

Równoległe kontenerów i obiektów

Algorytmy równoległe

Anulowanie w PPL

Obsługa wyjątków w Runtime współbieżności

Najlepszych praktyk w bibliotece agentów asynchroniczne

Najważniejsze wskazówki ogólne w czasie wykonywania współbieżności

Inne zasoby

Współbieżność Runtime najlepszych praktyk.