Condividi tramite


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:

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.

[Torna all'inizio]

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.

[Torna all'inizio]

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.

[Torna all'inizio]

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.

[Torna all'inizio]

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.

[Torna all'inizio]

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.

[Torna all'inizio]

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

[Torna all'inizio]

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.

[Torna all'inizio]

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.

[Torna all'inizio]

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++.

[Torna all'inizio]

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