Biblioteka wzorów równoległych — Najlepsze praktyki
W tym dokumencie opisano jak najlepiej Aby efektywnie wykorzystać równolegle Biblioteka wzorców (PPL).PPL przewiduje kontenerów ogólnego przeznaczenia, obiektów i algorytmy równoległości szczegółowymi zasadami wykonywania.
Aby uzyskać więcej informacji o PPL, zobacz Biblioteka równoległych wzorców (PLL).
Sekcje
Ten dokument zawiera następujące sekcje:
Nie zrównoleglij małych jednostek pętli
Ekspresowa równoległość na najwyższym możliwym poziomie
Użyj funkcji parallel_invoke do rozwiązywania problemów związanych z metodą projektowania algorytmów „Dziel i zwyciężaj”
Użyj anulowania lub obsługi wyjątków, aby zerwać z równoległą pętlą
Zrozum w jaki sposób unieważnienie i obsługa wyjątków wpływają na zniszczenie obiektu
Nie powtarzaj blokowania w pętli równoległej
Nie wykonuj operacji blokowania po anulowaniu czynności równoległej
Nie wpisuj do współdzielonych danych w pętli równoległej
Jeśli to możliwe, unikaj niezamierzonego współdzielenia
Upewnij się, że zmienne są ważne przez cały okres istnienia zadania
Nie zrównoleglij małych jednostek pętli
Hyper organów stosunkowo niewielką pętli może powodować skojarzone planowanie napowietrznych ma przewyższają zalety przetwarzanie równoległe.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ła, aby korzystać z narzut na przetwarzanie równoległe.Można zwiększyć wydajność ta pętla przez wykonanie większej ilości pracy w treści pętli lub wykonując pętli pojedynczo.
[U góry]
Ekspresowa równoległość na najwyższym możliwym poziomie
Gdy zrównoleglenia kodu tylko na niskim poziomie, może wprowadzić konstrukcja rozwidlenia sprzężenia, która nie podlega skalowaniu jako liczba procesorów wzrasta.A rozwidlenia join konstrukcji jest konstrukcji, gdzie jedno zadanie dzieli swojej pracy na mniejszych równoległych podzadania i czeka na te podzadania do końca.Podzadania można rekursywnie divide do dodatkowych podzadań.
Chociaż model sprzężenia rozwidlenia mogą być przydatne w rozwiązywaniu 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żdej iteracji pętli jest niezależna, można znaczną część pracy, zrównoleglają, jak pokazano w następującym 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 sprzężenia rozwidlenia przez wywołanie ProcessImage funkcja w pętli.Każde wywołanie ProcessImage nie zwraca przed zakończeniem 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);
});
}
Jeśli każdej iteracji pętli równoległych albo wykonuje prawie żadnej pracy lub pracy, która jest wykonywana przez równoległe pętli jest nierówne, to znaczy, niektóre iteracji pętli trwać dłużej niż inne, planowanie napowietrznych który jest wymagane do talerza często i sprzężenia pracy może przeważyć 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ównoleglają zewnętrznej pętli przed zrównoleglenia wewnętrznej pętli lub użyj innej konstrukcji równolegle, takich jak rurociąg.Poniższy przykład modyfikuje ProcessImages funkcji należy użyć 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 wykorzystuje rurociąg przeprowadzać równolegle do przetwarzania obrazu, zobacz Wskazówki: tworzenie sieci przetwarzania obrazów.
[U góry]
Użyj funkcji parallel_invoke do rozwiązywania problemów związanych z metodą projektowania algorytmów „Dziel i zwyciężaj”
A Dzielenie i zdobyć problem jest formą konstrukcja rozwidlenia 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 dzielenie i zdobyć.parallel_invoke Algorytm ma bardziej zwięzłe składnię niż obiekty grupy zadań i jest przydatne, gdy użytkownik ma stałą liczbę zadań równoległych.
Poniższy przykład ilustruje użycie parallel_invoke algorytm, który implementuje bitonic algorytm sortowania.
// 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ę tego przykładu, zobacz Porady: używanie parallel_invoke do napisania procedury sortowania równoległego.Aby uzyskać więcej informacji o parallel_invoke algorytmu, zobacz Algorytmy równoległe.
[U góry]
Użyj anulowania lub obsługi wyjątków, aby zerwać z równoległą pętlą
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łoś wyjątek w treści funkcji pracy zadania.Mechanizm odwołania jest bardziej wydajne niż na anulowanie drzewo równoległą pracę obsługi wyjątków.A równoległą pracę drzewa jest grupą grup powiązanych zadań, w których niektóre zadania grupy zawierają inne grupy zadań.Mechanizm anulowania anuluje grupę zadań i jej grupy zadań podrzędnych w sposób góra dół.I odwrotnie obsługi wyjątków działa w sposób dół góra i musisz anulować każdej grupy zadań podrzędnych niezależnie jako wyjątek rozprzestrzenia się 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ń.Na przykład, rozważmy następującą funkcję parallel_find_any, która wyszukuje wartości 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;
}
Algorytmy równoległe używają grup zadań, gdy jeden z równoległych iteracji anuluje nadrzędna grupa zadań, ogólne zadanie zostało anulowane.Aby uzyskać pełną wersję tego przykładu, zobacz Porady: użyj anulowania, aby przerwać pętlę równoległą.
Chociaż obsługa wyjątków jest mniej wydajne sposobem na anulowanie 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 kontener std::list , która 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 funkcja pracy ma być wywoływana na każdy element drzewa.W poniższym przykładzie pokazano search_for_value funkcja, która wyszukuje wartość w dołączonym tree obiektu.search_for_value Funkcja używa funkcji pracy, która zgłasza wyjątek, gdy bieżący element drzewa pasuje do podanych wartości.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ę tego przykładu, zobacz Porady: Użyj obsługi wyjątków, aby przerwać pętlę równoległą.
Aby uzyskać więcej ogólnych 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 we współbieżności środowiska wykonawczego.
[U góry]
Zrozum w jaki sposób unieważnienie i 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żna dla aplikacji, taką jak zwalnianie zasobu.Ponadto anulowanie zadania może spowodować wyjątek, aby propagacja destruktor obiektu i spowodować Niezdefiniowany zachowanie w aplikacji.
W poniższym przykładzie Resource klasy opisuje zasób i Container klasy opisuje kontener, który zawiera zasoby.W jego destruktora Container klasy wywołania cleanup metody na dwóch jego Resource członków w równolegle, a następnie wywołania 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ż ten wzór nie ma problemów z własnej, należy wziąć pod uwagę następujący kod, który uruchamia zadania dwóch równolegle.Tworzy pierwsze zadanie 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 odwołanie występuje po Container tworzony jest obiekt 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 będzie zachowywać się inaczej niż można by oczekiwać:
Powoduje anulowanie zadania nadrzędnego 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 wewnętrzny wyjątek zadania podrzędnego.Ponieważ Container destruktora nie obsługuje ten wyjątek, wyjątek są propagowane w górę i trzeciego zasób nie zostanie zwolniona.
Wyjątek, który jest generowany przez zadanie podrzędne rozchodzi się poprzez Container destruktora.Wyrzucanie z destruktora stawia aplikacji w stanie niezdefiniowany.
Zaleca się, że nie należy wykonywać operacji krytycznych, takich jak zwalnianiu zasoby w widoku zadania, o ile nie można zagwarantować, że zadania te nie zostaną anulowane.Zalecane jest również nieużywanie obsługi funkcji, które mogą rzucać w destruktora typów.
[U góry]
Nie powtarzaj blokowania w pętli równoległej
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 umożliwia tworzenie wielu wątków w krótkim okresie czasu.
Współbieżność Runtime wykonuje dodatkową pracę podczas zadania kończy lub wspólnie blokuje lub plonów.Po jednej równoległy pętli bloki iteracji, środowiska wykonawczego może zacząć innego iteracji.Jeśli nie ma żadnych dostępnych bezczynności wątków, środowisko wykonawcze tworzy nowy wątek.
Gdy ciała równolegle od czasu do czasu pętli bloki, mechanizm ten pomaga zmaksymalizowania ogólnej przepustowości zadania.Jednak gdy zablokować dużo iteracji, środowiska wykonawczego może utworzyć wiele wątków, aby uruchomić dodatkowej pracy.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 nowego wątku w celu uruchomienia dodatkowej pracy zawsze send nazywa się.
// 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);
});
}
Firma Microsoft zaleca, aby zreorganizujesz kod w celu uniknięcia tego wzorca.W tym przykładzie, można uniknąć tworzenia dodatkowych wątków poprzez wywołanie send w serial for pętli.
[U góry]
Nie wykonuj operacji blokowania po anulowaniu czynności równoległej
Jeśli to możliwe, nie należy wykonywać operacji blokowania przed wywołaniem concurrency::task_group::cancel lub concurrency::structured_task_group::cancel metoda aby anulować równoległą pracę.
Podczas zadania wykonywania spółdzielni blokowanie operacji, środowiska wykonawczego można wykonać inne prace podczas pierwszego zadania czeka na dane.Środowisko wykonawcze zmienia harmonogram zadań oczekujących, gdy go odblokowuje.Środowisko wykonawcze zazwyczaj zmienia harmonogram zadań, które były bardziej niedawno odblokowane przed jej zmienia harmonogram zadań, które były mniej niedawno odblokowany.W związku z tym środowiska wykonawczego może zaplanować niepotrzebnej pracy podczas operacji blokowania, co prowadzi do zmniejszenia wydajności.W związku z tym podczas wykonywania operacji blokowania zanim anulujesz równoległą pracę, operacja blokująca może 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 funkcja, która wyszukuje element dostarczonej tablicy, który spełnia podane funkcji predykatu.Kiedy funkcja predykatu zwraca true, równolegle 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 inną pracę tylko wtedy, gdy wykonuje zadanie spółdzielni blokowanie połączenia, takie jak numer telefonu, aby concurrency::critical_section::lock.
Poniższy przykład pokazuje, jak zapobiec niepotrzebnej pracy i tym samym poprawić wydajność.W tym przykładzie anuluje 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;
}
[U góry]
Nie wpisuj do współdzielonych danych w pętli równoległej
Współbieżność środowiska wykonawczego zawiera kilka struktur danych, na przykład, concurrency::critical_section, że synchronizacja równoczesny dostęp do danych udostępnionych.Te struktury danych są przydatne w wielu przypadkach, na przykład, gdy wiele zadań wymaga rzadko współdzielony dostęp do zasobu.
Rozważmy następujący przykład, który używa concurrency::parallel_for_each algorytm i critical_section obiekt, aby obliczyć liczbę elementów prime w 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 szereguje pętli.Ponadto kiedy obiekt współbieżność Runtime wykonuje operację blokowania, harmonogram może utworzyć dodatkowe wątku do wykonania 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, aplikacji można wykonywać słabo lub wprowadzić stan zasobów.
Definiuje PPL concurrency::combinable klasy, który pomaga wyeliminować stan udostępnionego poprzez zapewnienie dostępu do zasobów udostępnionych w sposób wolny blokady.combinable Klasa oferuje pamięci lokalnej wątku, który umożliwia wykonywanie obliczeń szczegółowymi zasadami i następnie scalić tych obliczeniach 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łasną lokalną kopię suma.W poniższym przykładzie użyto concurrency::combinable::combine metoda do scalenia lokalnych obliczeniach 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ę tego przykładu, zobacz Porady: korzystanie z wyników połączonych do poprawiania wydajności.Aby uzyskać więcej informacji dotyczących klasy combinable, wejdź na Równoległe kontenery oraz obiekty.
[U góry]
Jeśli to możliwe, unikaj niezamierzonego współdzielenia
Udostępnianie fałszywe występuje wówczas, gdy wiele równoczesnych zadań uruchomionych na oddzielnych procesorów zapisu zmiennych, które znajdują się na tej samej linii pamięci podręcznej.Jedno zadanie zapisuje się do jednej ze zmiennych, unieważnienia wiersza pamięci podręcznej dla obu zmiennych.Każdy procesor musi ponownie wczytać wiersza pamięci podręcznej ilekroć dany wiersz pamięci podręcznej jest unieważnione.W związku z tym udostępnianie FAŁSZ może spowodować obniżenie wydajności w aplikacji.
Prosty przykład pokazuje dwóch poniższych równoczesnych zadań każdego przyrost wartości zmiennej counter udostępnionych.
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ć na wymianie 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ńczeniu 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 aby upewnić się, że zmienne liczników są w pamięci podręcznej oddzielnych wierszach.Poniższy przykład wyrównuje zmienne count1 i count2 w granicach 64-bajtowe.
__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 podczas muszą współużytkować dane pomiędzy zadania.combinable Klasy tworzy lokalnej wątku zmiennych w taki sposób, że udostępnianie FAŁSZ jest mniej prawdopodobne.Aby uzyskać więcej informacji dotyczących klasy combinable, wejdź na Równoległe kontenery oraz obiekty.
[U góry]
Upewnij się, że zmienne są ważne przez cały okres istnienia zadania
Gdy wyrażenie lambda podać do grupy zadań lub algorytm równoległy, klauzula przechwytywania określa, czy treść Wyrażenie lambda uzyskuje dostęp do zmiennych w zasięgu, przez wartość lub przez odwołanie.Podczas przekazywania zmiennych do wyrażenia lambda przez odniesienie, musisz zagwarantować, że okres istnienia tej zmiennej utrzymuje się do zakończenia zadania.
Rozważmy następujący przykład, który definiuje object klasy i perform_action funkcji.perform_action Funkcja tworzy object zmiennej i wykonuje pewne działania na tej zmiennej asynchronicznie.Ponieważ zadanie nie jest gwarantowana na zakończenie przed perform_action , funkcja zwraca będzie ulec awarii lub wykazują 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ść do tego zadania.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 Państwo jego zmiany do tej zmiennej nie pojawiają się w oryginalnej kopii.
W poniższym przykładzie użyto concurrency::task_group::wait metoda, aby upewnić się, że przed zakończeniem zadania 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 zakończy teraz zanim funkcja zwraca, perform_action funkcja nie jest już działa asynchronicznie.
Poniższy przykład modyfikuje perform_action przez funkcję odwołanie 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();
});
}
Wskaźnik służy również do kontroli 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 Wyrażenia lambda w języku C++.
[U góry]
Zobacz też
Zadania
Wskazówki: tworzenie sieci przetwarzania obrazów
Porady: używanie parallel_invoke do napisania procedury sortowania równoległego
Porady: użyj anulowania, aby przerwać pętlę równoległą
Porady: korzystanie z wyników połączonych do poprawiania wydajności
Koncepcje
Biblioteka równoległych wzorców (PLL)
Równoległe kontenery oraz obiekty
Obsługa wyjątków we współbieżności środowiska wykonawczego
Biblioteka agentów asynchronicznych — Najlepsze praktyki
Współbieżność środowiska wykonawczego — Najlepsze praktyki ogólne