Compartilhar via


Práticas recomendadas na Biblioteca de Padrões Paralelos

Este documento descreve a melhor forma de usar a PPL (Biblioteca de Padrões Paralelos) de forma efetiva. A PPL fornece contêineres, objetos e algoritmos de uso geral para a execução de paralelismo refinado.

Para saber mais sobre a PPL, veja PPL (Biblioteca de Padrões Paralelos).

Seções

Este documento contém as seguintes seções:

Não paralelizar corpos de loop pequeno

A paralelização de corpos de loop relativamente pequeno pode fazer com que a sobrecarga de agendamento associada supere os benefícios do processamento paralelo. Considere o exemplo a seguir, que adiciona cada par de elementos em duas matrizes.

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

A carga de trabalho para cada iteração de loop paralelo é muito pequena para se beneficiar da sobrecarga do processamento paralelo. É possível melhorar o desempenho desse loop por meio de trabalhos adicionais no corpo dele ou de sua execução em série.

[Parte superior]

Expresse paralelismo no mais alto nível possível

Ao paralelizar o código somente em nível baixo, é possível introduzir um constructo fork-join que não é dimensionado à medida que o número de processadores aumenta. Um constructo fork-join é aquele em que uma tarefa divide o trabalho em subtarefas paralelas menores e aguarda a conclusão delas. Cada subtarefa pode se dividir recursivamente em subtarefas adicionais.

Embora o modelo fork-join possa ser útil para resolver uma variedade de problemas, há situações em que a sobrecarga de sincronização pode diminuir a escalabilidade. Por exemplo, considere o código serial a seguir que processa dados de imagem.

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

Como cada iteração de loop é independente, é possível paralelizar grande parte do trabalho, conforme mostrado no exemplo a seguir. Este exemplo usa o algoritmo concurrency::parallel_for para paralelizar o loop externo.

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

O exemplo a seguir ilustra um constructo fork-join chamando a função ProcessImage em um loop. Cada chamada para ProcessImage não é retornada até que cada subtarefa seja concluída.

// 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 cada iteração do loop paralelo executar quase nenhum trabalho ou as execuções nele forem desequilibradas (ou seja, algumas iterações de loop demorarem mais do que outras), a sobrecarga de agendamento necessária a fim de criar bifurcação e junção para o trabalho frequentemente poderá superar o benefício da execução paralela. Essa sobrecarga aumenta à medida que o número de processadores aumenta.

Para reduzir a quantidade de sobrecarga de agendamento neste exemplo, é possível paralelizar loops externos antes de paralelizar loops internos ou usar outro constructo paralelo, como pipelining. O exemplo a seguir modifica a função ProcessImages a fim de usar o algoritmo concurrency::parallel_for_each para paralelizar o loop externo.

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

Para obter um exemplo semelhante que usa um pipeline para executar o processamento de imagens em paralelo, veja Passo a passo: criar uma rede de processamento de imagens.

[Parte superior]

Use parallel_invoke para resolver problemas de dividir e conquistar

Um problema de divide-and-conquer é uma forma de constructo fork-join que usa recursão para dividir uma tarefa em subtarefas. Além das classes concurrency::task_group e concurrency::structured_task_group, é possível usar o algoritmo concurrency::parallel_invoke para solucionar problemas de divide-and-conquer. O algoritmo parallel_invoke tem uma sintaxe mais sucinta do que os objetos de grupo de tarefas e é útil quando você tem um número fixo de tarefas paralelas.

O exemplo a seguir ilustra o uso do algoritmo parallel_invoke para implementar o algoritmo de classificação bitônica.

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

Para reduzir a sobrecarga, o algoritmo parallel_invoke executa a última da série de tarefas no contexto da chamada.

Para obter a versão completa deste exemplo, veja Como usar parallel_invoke para escrever uma rotina de classificação paralela. Para saber mais sobre o algoritmo parallel_invoke, confira Algoritmos paralelos.

[Parte superior]

Usar cancelamento ou tratamento de exceção para quebrar de um loop paralelo

A PPL fornece duas maneiras de cancelar o trabalho paralelo executado por um grupo de tarefas ou um algoritmo paralelo. Uma maneira é usar o mecanismo de cancelamento fornecido pelas classes concurrency::task_group e concurrency::structured_task_group. A outra é lançar uma exceção no corpo da função de trabalho de uma tarefa. O mecanismo de cancelamento é mais eficiente do que o tratamento de exceções ao cancelar uma árvore de trabalho paralelo. Uma árvore de trabalho paralelo é um grupo de grupos de tarefas relacionados, em que alguns contêm grupos de tarefas adicionais. O mecanismo de cancelamento cancela um grupo de tarefas e seus grupos de tarefas filho de maneira descendente. Por outro lado, o tratamento de exceção funciona de maneira ascendente e deve cancelar cada grupo de tarefas filho independentemente à medida que a exceção se propaga para cima.

Ao trabalhar diretamente com um objeto de grupo de tarefas, use os métodos concurrency::task_group::cancel ou concurrency::structured_task_group::cancel para cancelar o trabalho que pertence ao grupo de tarefas. Para cancelar um algoritmo paralelo, por exemplo, parallel_for, crie um grupo de tarefas pai e cancele-o em seguida. Por exemplo, considere a função parallel_find_any a seguir, que pesquisa um valor em uma matriz em paralelo.

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

Como os algoritmos paralelos usam grupos de tarefas, a tarefa geral é cancelada quando uma das iterações paralelas cancela o grupo de tarefas pai. Para obter a versão completa deste exemplo, veja Como usar o cancelamento para interromper um loop paralelo.

Embora o tratamento de exceção seja uma maneira menos eficiente de cancelar o trabalho paralelo do que o mecanismo de cancelamento, há casos em que ele é apropriado. Por exemplo, o método for_all a seguir executa recursivamente uma função de trabalho em cada nó de uma estrutura tree. Neste exemplo, o membro de dados _children é uma std::list que contém objetos tree.

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

O chamador do método tree::for_all pode lançar uma exceção caso a função de trabalho não precise ser chamada em cada elemento da árvore. O exemplo a seguir mostra a função search_for_value, que pesquisa um valor no objeto tree fornecido. A função search_for_value usa uma função de trabalho que lança uma exceção quando o elemento atual da árvore corresponde ao valor fornecido. A função search_for_value usa um bloco try-catch para capturar a exceção e imprimir o resultado no 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();   
}

Para obter a versão completa deste exemplo, confira Como usar o tratamento de exceção para interromper um loop paralelo.

Para obter informações mais gerais sobre os mecanismos de cancelamento e tratamento de exceções fornecidos pela PPL, confira Cancelamento na PPL e Tratamento de exceções.

[Parte superior]

Entenda como o cancelamento e o tratamento de exceções afetam a destruição de objetos

Em uma árvore de trabalho paralelo, o cancelamento de uma tarefa impede a execução das tarefas filho. Isso pode causar problemas quando uma das tarefas filho executa uma operação importante para o aplicativo, como a liberação de um recurso. Além disso, o cancelamento de tarefas pode fazer com que uma exceção se propague por meio de um destruidor de objetos e cause um comportamento indefinido no aplicativo.

No exemplo a seguir, a classe Resource descreve um recurso e a classe Container descreve um contêiner que contém recursos. No destruidor, a classe Container chama o método cleanup em dois de seus membros Resource em paralelo e, em seguida, chama o método cleanup em seu terceiro 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;
};

Embora esse padrão não tenha problemas por si só, considere o código a seguir que executa duas tarefas em paralelo. A primeira cria um objeto Container e a segunda cancela a tarefa geral. Para ilustração, o exemplo usa dois objetos concurrency::event para garantir que o cancelamento ocorra após a criação do objeto Container e que o objeto Container seja destruído após a operação de cancelamento.

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

Esse exemplo gera a saída a seguir:

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

Este exemplo de código contém os seguintes problemas que podem fazer com que ele se comporte de maneira diferente do esperado:

  • O cancelamento da tarefa pai faz com que a tarefa filho, a chamada para concurrency::parallel_invoke, também seja cancelada. Portanto, esses dois recursos não são liberados.

  • O cancelamento da tarefa pai faz com que a tarefa filho lance uma exceção interna. Como o destruidor Container não trata essa exceção, ela é propagada para cima e o terceiro recurso não é liberado.

  • A exceção lançada pela tarefa filho se propaga pelo destruidor Container. O lançamento por meio de um destruidor coloca o aplicativo em um estado indefinido.

Recomenda-se não executar operações críticas, como liberação de recursos, em tarefas, a menos que seja possível garantir que essas tarefas não serão canceladas. Também é recomendado não usar a funcionalidade de runtime, que pode fazer um lançamento no destruidor de seus tipos.

[Parte superior]

Não bloqueie repetidamente em um loop paralelo

Um loop paralelo como concurrency::parallel_for ou concurrency::parallel_for_each, que é dominado por operações de bloqueio, pode fazer com que o runtime crie muitos threads em um curto período de tempo.

O Runtime de Simultaneidade executa o trabalho adicional quando uma tarefa é concluída ou realiza o bloqueio ou produção cooperativamente. Quando uma iteração de loop paralelo é bloqueada, o runtime pode iniciar outra. Quando não há threads ociosos disponíveis, o runtime cria um novo.

Quando o corpo de um loop paralelo é bloqueado ocasionalmente, esse mecanismo ajuda a maximizar a taxa de transferência geral da tarefa. No entanto, quando muitas iterações são bloqueadas, o runtime pode criar muitos threads para executar o trabalho adicional. Isso pode levar a condições de pouca memória ou má utilização dos recursos de hardware.

Considere o exemplo a seguir que chama a função concurrency::send em cada iteração de um loop parallel_for. Como o send é bloqueado cooperativamente, o runtime cria um thread para executar o trabalho adicional toda vez que send é chamado.

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

Recomenda-se refatorar o código para evitar esse padrão. Neste exemplo, é possível evitar a criação de threads adicionais chamando send em um loop serial for.

[Parte superior]

Não execute operações de bloqueio ao cancelar o trabalho paralelo

Quando possível, não execute operações de bloqueio antes de chamar o método concurrency::task_group::cancel ou concurrency::structured_task_group::cancel para cancelar o trabalho paralelo.

Quando uma tarefa executa uma operação de bloqueio cooperativa, o runtime pode realizar outro trabalho enquanto a primeira tarefa aguarda dados. O runtime reagenda a tarefa em espera quando ela é desbloqueada. Normalmente, ele reagenda as tarefas que foram desbloqueadas mais recentemente antes de reprogramar as tarefas que foram desbloqueadas menos recentemente. Portanto, ele pode agendar trabalhos desnecessários durante a operação de bloqueio, o que leva à diminuição do desempenho. Assim, ao executar uma operação de bloqueio antes de cancelar o trabalho paralelo, a operação de bloqueio pode atrasar a chamada para cancel. Isso faz com que outras tarefas executem trabalhos desnecessários.

Considere o exemplo a seguir que define a função parallel_find_answer. Essa função pesquisa um elemento da matriz fornecida que atende à função de predicado fornecida. Quando a função de predicado retorna true, a função de trabalho paralelo cria um objeto Answer e cancela a tarefa geral.

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

O operador new executa uma alocação de heap, que pode ser bloqueada. O runtime executa outro trabalho somente quando a tarefa executa uma chamada de bloqueio cooperativa, como uma chamada para concurrency::critical_section::lock.

O exemplo a seguir mostra como evitar trabalho desnecessário e, assim, melhorar o desempenho. Este exemplo cancela o grupo de tarefas antes de alocar o armazenamento para o objeto 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;
}

[Parte superior]

Não gravar em dados compartilhados em um loop paralelo

O Runtime de Simultaneidade fornece várias estruturas de dados, por exemplo, concurrency::critical_section, que sincronizam o acesso simultâneo a dados compartilhados. Essas estruturas de dados são úteis em muitos casos, por exemplo, quando diversas tarefas raramente exigem acesso compartilhado a um recurso.

Considere o exemplo a seguir que usa o algoritmo concurrency::parallel_for_each e um objeto critical_section para calcular a contagem de números primos em um objeto std::array. Este exemplo não é dimensionado porque cada thread deve aguardar para acessar a variável compartilhada 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();
});

Ele também pode levar a um desempenho ruim porque a operação de bloqueio frequente serializa efetivamente o loop. Além disso, quando um objeto de Runtime de Simultaneidade executa uma operação de bloqueio, o agendador pode criar um thread adicional para realizar outro trabalho enquanto o primeiro thread aguarda dados. Se o runtime criar muitos threads porque muitas tarefas estão aguardando dados compartilhados, o aplicativo poderá ter um desempenho insatisfatório ou entrar em um estado de poucos recursos.

A PPL define a classe concurrency::combinable, que ajuda a eliminar o estado compartilhado fornecendo acesso a recursos compartilhados sem bloqueio. A classe combinable fornece o armazenamento local de thread, que permite realizar cálculos refinados e mesclar esses cálculos em um resultado final. É possível pensar em um objeto combinable como uma variável de redução.

O exemplo a seguir modifica o anterior usando um objeto combinable em vez de um objeto critical_section para calcular a soma. Este exemplo é dimensionado porque cada thread contém a própria cópia local da soma. Ele usa o método concurrency::combinable::combine para mesclar os cálculos locais no resultado final.

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

Para obter a versão completa deste exemplo, confira Como usar combinável para melhorar o desempenho. Para mais informações sobre a classe combinable, confira Contêineres e objetos paralelos.

[Parte superior]

Quando possível, evite o compartilhamento falso

O compartilhamento falso ocorre quando diversas tarefas simultâneas executadas em processadores separados gravam em variáveis ​​localizadas na mesma linha de cache. Quando uma tarefa grava em uma das variáveis, a linha de cache para ambas ​​é invalidada. Cada processador deve recarregar a linha de cache toda vez que ela é invalidada. Portanto, o compartilhamento falso pode diminuir o desempenho do aplicativo.

O exemplo básico a seguir mostra duas tarefas simultâneas em que cada uma incrementa uma variável de contador compartilhada.

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

Para eliminar o compartilhamento de dados entre as duas tarefas, é possível modificar o exemplo para usar duas variáveis ​​de contador. Este exemplo calcula o valor final do contador após a conclusão das tarefas. No entanto, ele ilustra o compartilhamento falso porque as variáveis count1 e count2 provavelmente estão localizadas na mesma linha de 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;

Uma maneira de eliminar o compartilhamento falso é certificar-se de que as variáveis ​​do contador estejam em linhas de cache separadas. O exemplo a seguir alinha as variáveis count1 e count2 nos limites de 64 bytes.

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

Este exemplo pressupõe que o tamanho do cache de memória seja de 64 bytes ou menos.

Recomenda-se usar a classe concurrency::combinable quando é preciso compartilhar dados entre tarefas. A classe combinable cria variáveis ​​locais de thread de maneira que o compartilhamento falso seja menos provável. Para mais informações sobre a classe combinable, confira Contêineres e objetos paralelos.

[Parte superior]

Verifique se as variáveis são válidas durante todo o tempo de vida de uma tarefa

Quando você fornece uma expressão lambda para um grupo de tarefas ou algoritmo paralelo, a cláusula de captura especifica se o corpo da expressão lambda acessa variáveis ​​no escopo delimitador por valor ou por referência. Ao transmitir variáveis ​​para uma expressão lambda por referência, é necessário garantir que o tempo de vida da variável persista até que a tarefa seja concluída.

Considere o exemplo a seguir que define a classe object e a função perform_action. A função perform_action cria uma variável object e executa algumas ações nessa variável de maneira assíncrona. Como não é garantido que a tarefa termine antes do retorno da função perform_action, o programa travará ou exibirá um comportamento não especificado se a variável object for destruída quando a tarefa estiver em execução.

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

Dependendo dos requisitos do aplicativo, é possível usar uma das técnicas a seguir para garantir que as variáveis ​​permaneçam válidas durante toda a vida útil de cada tarefa.

O exemplo a seguir transmite a variável object por valor para a tarefa. Portanto, a tarefa opera na própria cópia da variável.

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

Como a variável object é transmitida por valor, quaisquer alterações de estado que ocorram nela não aparecem na cópia original.

O exemplo a seguir usa o método concurrency::task_group::wait para garantir que a tarefa termine antes do retorno da função perform_action.

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

Como a tarefa agora termina antes do retorno da função, a função perform_action não se comporta mais de maneira assíncrona.

O exemplo a seguir modifica a função perform_action para fazer uma referência à variável object. O chamador deve garantir que o tempo de vida da variável object seja válido até que a tarefa termine.

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

Também é possível usar um ponteiro para controlar o tempo de vida de um objeto transmitido para um grupo de tarefas ou algoritmo paralelo.

Para obter mais informações sobre expressões lambda, consulte Expressões lambda.

[Parte superior]

Confira também

Práticas recomendadas do runtime de simultaneidade
Biblioteca de padrões paralelos (PPL)
Contêineres e objetos em paralelo
Algoritmos paralelos
Cancelamento no PPL
Tratamento de exceção
Instruções passo a passo: criando uma rede de processamento de imagem
Como usar parallel_invoke para escrever uma rotina de classificação em paralelo
Como usar cancelamento para interromper um loop paralelo
Como usar combinável para melhorar o desempenho
Práticas recomendadas na biblioteca de agentes assíncronos
Práticas recomendadas gerais no runtime de simultaneidade