Procedure consigliate nella libreria PPL (Parallel Patterns Library)
In questo documento viene descritto come ottimizzare l'uso della libreria PPL (Parallel Patterns Library). In tale libreria sono disponibili contenitori, oggetti e algoritmi di uso generale che consentono di eseguire un parallelismo accurato.
Per altre informazioni sul PPL, vedere Parallel Patterns Library (PPL).
Sezioni
Questo documento contiene le seguenti sezioni:
Usare parallel_invoke per risolvere i problemi di divisione e conquista
Usare la gestione dell'annullamento o delle eccezioni per interrompere un ciclo parallelo
Non eseguire operazioni di blocco quando si annulla il lavoro parallelo
Assicurarsi che le variabili siano valide per tutta la durata di un'attività
Non parallelizzare corpi di ciclo piccolo
La parallelizzazione di corpi di ciclo di dimensioni relativamente ridotte può determinare un sovraccarico della pianificazione associata che annulla i vantaggi derivanti dall'elaborazione in parallelo. Si consideri l'esempio seguente, in cui ogni coppia di elementi viene aggiunta in due matrici.
// 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.
}
Il volume del carico di lavoro per ogni iterazione del ciclo parallelo è troppo piccolo per poter trarre vantaggio dal sovraccarico dell'elaborazione in parallelo. È possibile migliorare le prestazioni di questo ciclo eseguendo un maggior volume di lavoro nel corpo del ciclo oppure eseguendo il ciclo in modalità seriale.
Parallelismo espresso al massimo livello possibile
Quando il codice viene parallelizzato solo a un livello basso, è possibile introdurre un costrutto fork-join che non viene ridimensionato con l'aumento del numero di processori. Un costrutto fork-join è un costrutto in cui un'attività divide il lavoro in sottoattività parallele più piccole e attende il completamento di tali sottoattività. Ciascuna sottoattività può a sua volta essere suddivisa in modo ricorsivo in ulteriori sottoattività.
Sebbene il modello fork-join possa risultare utile per risolvere vari problemi, in alcune situazioni il sovraccarico della sincronizzazione può diminuire la scalabilità. Si consideri, ad esempio, il codice seriale seguente che elabora i dati di immagine.
// 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);
}
Poiché ogni iterazione del ciclo è indipendente, è possibile parallelizzare gran parte del lavoro, come illustrato nell'esempio seguente. In questo esempio viene usato l'algoritmo concurrency::p arallel_for per parallelizzare il ciclo esterno.
// 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);
}
Nell'esempio seguente viene illustrato un costrutto fork-join mediante la chiamata alla funzione ProcessImage
in un ciclo. Ogni chiamata a ProcessImage
non comporta alcuna risposta fino al completamento di ciascuna sottoattività.
// 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);
});
}
Se ogni iterazione del ciclo parallelo non esegue quasi alcun lavoro oppure il lavoro eseguito dal ciclo parallelo non è bilanciato, ovvero alcune iterazioni del ciclo richiedono più tempo di altre, il sovraccarico di pianificazione richiesto per eseguire con frequenza le operazioni di fork e join può annullare il vantaggio dell'esecuzione parallela. Questo sovraccarico aumenta con l'aumentare del numero di processori.
Per ridurre la quantità di sovraccarico di pianificazione in questo esempio, è possibile parallelizzare i cicli esterni prima di quelli interni oppure usare un altro costrutto parallelo come pipelining. Nell'esempio seguente la ProcessImages
funzione viene modificata in modo da usare concurrency ::p arallel_for_each per parallelizzare il ciclo esterno.
// 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);
});
}
Per un esempio simile che usa una pipeline per eseguire l'elaborazione di immagini in parallelo, vedere Procedura dettagliata: Creazione di una rete di elaborazione immagini.
Usare parallel_invoke per risolvere i problemi di divisione e conquista
Un problema di divisione e conquista è una forma del costrutto fork-join che usa la ricorsione per suddividere un'attività in sottoattività. Oltre alle classi concurrency::task_group e concurrency::structured_task_group , è anche possibile usare l'algoritmo concurrency::p arallel_invoke per risolvere i problemi di divisione e conquista. L'algoritmo parallel_invoke
ha una sintassi più concisa rispetto agli oggetti gruppo di attività ed è utile quando è presente un numero fisso di attività parallele.
Nell'esempio seguente viene illustrato l'uso dell'algoritmo parallel_invoke
per implementare l'algoritmo di ordinamento bitonico.
// 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);
}
}
Per ridurre il sovraccarico, l'algoritmo parallel_invoke
esegue l'ultima delle serie di attività nel contesto di chiamata.
Per la versione completa di questo esempio, vedere Procedura: Usare parallel_invoke per scrivere una routine di ordinamento parallelo. Per altre informazioni sull'algoritmo parallel_invoke
, vedere Algoritmi paralleli.
Usare la gestione dell'annullamento o delle eccezioni per interrompere un ciclo parallelo
La libreria PPL fornisce due modi per annullare il lavoro parallelo che viene eseguito da un gruppo di attività o da un algoritmo parallelo. Un modo consiste nell'usare il meccanismo di annullamento fornito dalle classi concurrency::task_group e concurrency::structured_task_group . L'altro consiste nel generare un'eccezione nel corpo di una funzione lavoro dell'attività. Il meccanismo di annullamento è più efficace della gestione delle eccezioni per annullare un albero di lavoro parallelo. Un albero di lavoro parallelo è un gruppo di gruppi di attività correlati in cui alcuni gruppi di attività contengono altri gruppi di attività. Il meccanismo di annullamento annulla un gruppo di attività e i relativi gruppi di attività figlio dall'alto verso il basso. La gestione delle eccezioni funziona invece in ordine sequenziale dal basso verso l'alto e deve annullare ogni gruppo di attività figlio in modo indipendente in quanto l'eccezione si propaga verso l'alto.
Quando si lavora direttamente con un oggetto gruppo di attività, usare i metodi concurrency::task_group::cancel o concurrency::structured_task_group::cancel per annullare il lavoro appartenente a tale gruppo di attività. Per annullare un algoritmo parallelo, ad esempio parallel_for
, creare un gruppo di attività padre e annullarlo. Si consideri ad esempio la funzione seguente, parallel_find_any
, che esegue la ricerca di un valore in una matrice in parallelo.
// 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;
}
Poiché gli algoritmi paralleli usano i gruppi di attività, quando una delle iterazioni parallele annulla il gruppo di attività padre, viene annullata l'intera attività. Per la versione completa di questo esempio, vedere Procedura: Usare l'annullamento per interrompere un ciclo parallelo.
Anche se il meccanismo di annullamento della gestione delle eccezioni risulta meno efficace per annullare il lavoro parallelo, in alcune situazioni questo sistema è più appropriato. Ad esempio il metodo seguente, for_all
, esegue in modo ricorsivo una funzione lavoro in ciascun nodo di una struttura tree
. In questo esempio, il _children
membro dati è un std::list che contiene tree
oggetti .
// 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);
}
Il chiamante del metodo tree::for_all
può generare un'eccezione se non richiede che la funzione lavoro venga chiamata per ciascun elemento della struttura ad albero. Nell'esempio seguente viene illustrata la funzione search_for_value
che cerca un valore nell'oggetto tree
fornito. La funzione search_for_value
usa una funzione lavoro che genera un'eccezione quando l'elemento corrente della struttura ad albero corrisponde al valore fornito. La funzione search_for_value
usa un blocco try-catch
per acquisire l'eccezione e stampare il risultato nella console.
// 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();
}
Per la versione completa di questo esempio, vedere Procedura: Usare la gestione delle eccezioni per interrompere un ciclo parallelo.
Per informazioni più generali sui meccanismi di annullamento e gestione delle eccezioni forniti dalla libreria PPL, vedere Annullamento in PPL e Gestione delle eccezioni.
Informazioni su come l'annullamento e la gestione delle eccezioni influiscono sulla distruzione degli oggetti
In un albero di lavoro parallelo l'annullamento di un'attività impedisce l'esecuzione delle attività figlio. Ciò può comportare problemi se una delle attività figlio esegue un'operazione importante per l'applicazione, ad esempio liberare una risorsa. L'annullamento delle attività può inoltre provocare la propagazione di un'eccezione tramite un distruttore di oggetti e causare un comportamento non definito nell'applicazione.
Nell'esempio seguente la classe Resource
descrive una risorsa e la classe Container
descrive un contenitore che include le risorse. Nel rispettivo distruttore la classe Container
chiama il metodo cleanup
per due dei rispettivi membri Resource
in parallelo, quindi chiama il metodo cleanup
per il rispettivo terzo membro Resource
.
// 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;
};
Anche se questo modello non presenta problemi di per sé, si consideri il codice seguente, che esegue due attività in parallelo. La prima attività crea un oggetto Container
, mentre la seconda annulla l'intera attività. Ad esempio, nell'esempio vengono utilizzati due oggetti concurrency::event per assicurarsi che l'annullamento si verifichi dopo la creazione dell'oggetto e che l'oggetto Container
Container
venga eliminato definitivamente dopo che si verifica l'operazione di annullamento.
// 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;
}
Nell'esempio viene prodotto l'output seguente:
Container 1: Freeing resources...Exiting program...
Nell'esempio di codice sono presenti i problemi seguenti, che possono determinare un comportamento diverso da quello previsto:
L'annullamento dell'attività padre comporta anche l'annullamento dell'attività figlio, la chiamata a concurrency::p arallel_invoke. Di conseguenza, queste due risorse non vengono liberate.
L'annullamento dell'attività padre comporta la generazione di un'eccezione interna da parte dell'attività figlio. Poiché il distruttore
Container
non gestisce questa eccezione, l'eccezione viene propagata verso l'alto e la terza risorsa non viene liberata.L'eccezione che viene generata dall'attività figlio si propaga mediante il distruttore
Container
. La generazione da un distruttore imposta l'applicazione su uno stato non definito.
Si consiglia di non eseguire operazioni critiche nelle attività, ad esempio liberare risorse, a meno che non sia possibile garantire che queste attività non verranno annullate. Si consiglia inoltre di non usare la funzionalità di runtime che può generare un'eccezione nel distruttore dei tipi.
Non bloccare ripetutamente in un ciclo parallelo
Un ciclo parallelo, ad esempio concurrency::p arallel_for o concurrency::p arallel_for_each dominato da operazioni di blocco, può causare la creazione di molti thread in un breve periodo di tempo.
Il runtime di concorrenza esegue lavoro aggiuntivo quando un'attività termina oppure si blocca o cede volontariamente il controllo. Quando un'iterazione del ciclo parallelo si blocca, è possibile che il runtime avvii un'altra iterazione. Quando non sono disponibili thread inattivi, il runtime crea un nuovo thread.
Se il corpo di un ciclo parallelo si blocca occasionalmente, questo meccanismo contribuisce a ottimizzare la velocità dell'intera attività. Quando invece si blocca un numero elevato di iterazioni, il runtime potrebbe creare un numero elevato di thread per eseguire il lavoro aggiuntivo. Ciò può determinare condizioni di memoria insufficiente o di uso non appropriato delle risorse hardware.
Si consideri l'esempio seguente che chiama la funzione concurrency::send in ogni iterazione di un parallel_for
ciclo. Poiché send
si blocca in modo cooperativo, il runtime crea un nuovo thread per eseguire il lavoro aggiuntivo ogni volta che viene chiamato il metodo send
.
// 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);
});
}
È consigliabile effettuare il refactoring del codice per evitare questo modello. In questo esempio è possibile evitare la creazione di thread aggiuntivi chiamando il metodo send
in un ciclo seriale di for
.
Non eseguire operazioni di blocco quando si annulla il lavoro parallelo
Quando possibile, non eseguire operazioni di blocco prima di chiamare il metodo concurrency::task_group::cancel o concurrency::structured_task_group::cancel per annullare il lavoro parallelo.
Quando tramite un'attività viene effettuata un'operazione di blocco, mediante il runtime può essere eseguito altro lavoro mentre la prima attività resta in attesa dei dati. Quando si sblocca, il runtime ripianifica l'attività di attesa. Il runtime generalmente ripianifica prima le ultime attività sbloccate e poi quelle sbloccate meno di recente. Pertanto, il runtime potrebbe pianificare lavoro non necessario durante l'operazione di blocco, determinando una riduzione delle prestazioni. Di conseguenza, quando si esegue un'operazione di blocco prima di annullare lavoro parallelo, l'operazione di blocco può ritardare la chiamata a cancel
. Ciò comporta l'intervento di altre attività per l'esecuzione del lavoro non necessario.
Si consideri l'esempio seguente che definisce la funzione parallel_find_answer
. Tale funzione esegue la ricerca di un elemento della matrice fornita che soddisfa la funzione predicato specificata. Quando la funzione predicato restituisce true
, la funzione di lavoro parallela crea un Answer
oggetto e annulla l'attività complessiva.
// 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;
}
L'operatore new
esegue un'allocazione per l'heap, che potrebbe bloccarsi. Il runtime esegue altre operazioni solo quando l'attività esegue una chiamata di blocco cooperativo, ad esempio una chiamata a concurrency::critical_section::lock.
Nell'esempio seguente viene mostrato come evitare il lavoro non necessario e migliorare in tal modo le prestazioni. Questo esempio annulla il gruppo di attività prima di allocare lo spazio di archiviazione per l'oggetto Answer
.
// 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;
}
Non scrivere in dati condivisi in un ciclo parallelo
Il runtime di concorrenza fornisce diverse strutture di dati, ad esempio concurrency::critical_section, che sincronizzano l'accesso simultaneo ai dati condivisi. Queste strutture di dati sono utili in molti casi, ad esempio quando più attività richiedono raramente l'accesso condiviso a una risorsa.
Si consideri l'esempio seguente che usa l'algoritmo concurrency::p arallel_for_each e un critical_section
oggetto per calcolare il numero di numeri primi in un oggetto std::array . Questo esempio non è scalabile poiché ogni thread deve attendere per accedere alla variabile condivisa 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();
});
L'esempio può inoltre comportare una riduzione delle prestazioni in quanto l'operazione di blocco frequente serializza il ciclo in modo efficace. Inoltre, quando un oggetto runtime di concorrenza esegue un'operazione di blocco, l'utilità di pianificazione potrebbe creare un thread aggiuntivo per eseguire altro lavoro mentre il primo thread rimane in attesa dei dati. Se il runtime crea numerosi thread perché molte attività sono in attesa dei dati condivisi, è possibile che le prestazioni dell'applicazione si riducano notevolmente o che si passi a uno stato di risorse insufficienti.
Il PPL definisce la classe concurrency::combinable , che consente di eliminare lo stato condiviso fornendo l'accesso alle risorse condivise in modo senza blocchi. La classe combinable
fornisce l'archiviazione locale dei thread che consente di eseguire calcoli accurati e quindi di unire i calcoli in un risultato finale. È possibile considerare un oggetto combinable
come una variabile di riduzione.
Nell'esempio seguente si modifica l'esempio precedente mediante l'uso di un oggetto combinable
anziché un oggetto critical_section
per calcolare la somma. Questo esempio è scalabile, in quanto ogni thread è responsabile della propria copia locale della somma. In questo esempio viene usato il metodo concurrency::combinable::combine per unire i calcoli locali nel risultato finale.
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>());
Per la versione completa di questo esempio, vedere Procedura: Usare combinable per migliorare le prestazioni. Per altre informazioni sulla combinable
classe , vedere Contenitori e oggetti paralleli.
Quando possibile, evitare false condivisioni
La condivisione false si verifica quando più attività simultanee in esecuzione su processori separati scrivono in variabili che si trovano nella stessa riga della cache. Quando una sola attività scrive in una delle variabili, viene invalidata la riga della cache per entrambe le variabili. Ogni processore deve ricaricare la riga della cache ogni volta che questa viene invalidata. Pertanto, la falsa condivisione può compromettere le prestazioni nell'applicazione.
Nell'esempio di base seguente vengono illustrate due attività simultanee, ognuna delle quali incrementa una variabile di contatore condivisa.
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);
}
);
Per eliminare la condivisione dei dati tra le due attività, è possibile modificare l'esempio in modo che vengano usate due variabili di contatore. Questo esempio calcola il valore di contatore finale dopo che le attività sono state completate. L'esempio rappresenta tuttavia un caso di falsa condivisione poiché le variabili count1
e count2
possono essere collocate nella stessa riga della cache.
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;
Un modo per eliminare la falsa condivisione consiste nell'assicurarsi che le variabili di contatore si trovino su righe della cache separate. Nell'esempio seguente le variabili count1
e count2
vengono allineate su limiti di 64 byte.
__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;
In questo esempio si presuppone che la dimensione della cache in memoria sia pari o inferiore a 64 byte.
È consigliabile usare la classe concurrency::combinable quando è necessario condividere i dati tra le attività. La classe combinable
consente di creare le variabili di thread locali in modo da ridurre la probabilità che si verifichi la falsa condivisione. Per altre informazioni sulla combinable
classe , vedere Contenitori e oggetti paralleli.
Assicurarsi che le variabili siano valide per tutta la durata di un'attività
Quando si fornisce un'espressione lambda per un gruppo di attività o un algoritmo parallelo, la clausola di acquisizione specifica se il corpo dell'espressione lambda accede alle variabili nell'ambito di inclusione in base al valore o al riferimento. Quando si passano le variabili a un'espressione lambda in base al riferimento, è necessario garantire che tale variabile duri fino al completamento dell'attività.
Si consideri l'esempio seguente, che definisce la classe object
e la funzione perform_action
. La funzione perform_action
crea una variabile object
ed esegue una determinata azione su tale variabile in modo asincrono. Dal momento che non è sicuro che l'attività venga completata prima che la funzione perform_action
restituisca un risultato, il programma si arresterà in modo anomalo o seguirà un comportamento non specificato se la variabile object
viene eliminata durante l'esecuzione dell'attività.
// 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.
}
A seconda dei requisiti dell'applicazione, è possibile usare una delle tecniche seguenti per garantire che le variabili rimangano valide per l'intera durata di ogni attività.
Nell'esempio seguente la variabile object
viene passata all'attività in base al valore. Pertanto, l'attività agisce sulla propria copia della variabile.
// 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();
});
}
Poiché la variabile object
viene passata in base al valore, eventuali modifiche di stato apportate a questa variabile non vengono visualizzate nella copia originale.
Nell'esempio seguente viene usato il metodo concurrency::task_group::wait per assicurarsi che l'attività venga completata prima della restituzione della perform_action
funzione.
// 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();
}
Poiché ora l'attività viene completata prima che la funzione restituisca un risultato, la funzione perform_action
non si comporta più in modo asincrono.
Nell'esempio seguente la funzione perform_action
viene modificata in modo da accettare un riferimento alla variabile object
. Il chiamante deve garantire che la durata della variabile object
resti valida fino al termine dell'attività.
// Performs an action asynchronously.
void perform_action(object& obj, task_group& tasks)
{
// Perform some action on the object variable.
tasks.run([&obj] {
obj.action();
});
}
È inoltre possibile usare un puntatore per controllare la durata di un oggetto che viene passato a un gruppo di attività o a un algoritmo parallelo.
Per altre informazioni sulle espressioni lambda, vedere Espressioni lambda in C++.
Vedi anche
Procedure consigliate del runtime di concorrenza
PPL (Parallel Patterns Library)
Contenitori e oggetti paralleli
Algoritmi paralleli
Annullamento nella libreria PPL
Gestione delle eccezioni
Procedura dettagliata: creazione di una rete per l'elaborazione di immagini
Procedura: Usare parallel_invoke per scrivere una routine di ordinamento in parallelo
Procedura: Usare l'annullamento per interrompere un ciclo Parallel
Procedura: Usare la classe combinable per migliorare le prestazioni
Procedure consigliate nella libreria di agenti asincroni
Procedure consigliate generali nel runtime di concorrenza