Partilhar via


Práticas recomendadas para a biblioteca de padrões paralelos

Este documento descreve a melhor maneira para fazer uso efetivo da paralela padrões PPL (biblioteca). A PPL fornece os algoritmos, objetos e recipientes de uso gerais para executar o paralelismo refinado.

Para obter mais informações sobre a PPL, consulte Biblioteca paralela de padrões (PPL).

Seções

Este documento contém as seções a seguir:

  • Não paralelizar pequenos corpos de Loop

  • Expressar o paralelismo no nível mais alto possível

  • Use o parallel_invoke para solucionar problemas de "dividir e conquistar"

  • Use o cancelamento ou o tratamento de exceção para quebra de um Loop paralelo

  • Compreender como o cancelamento e a manipulação de exceção afetam a destruição de objeto

  • Não bloquear repetidamente em um Loop paralelo

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

  • Não gravar os dados compartilhados em um Loop paralelo

  • Quando possível, evitar o falso compartilhamento

  • Certifique-se de que as variáveis são válidas em todo o tempo de vida de uma tarefa

Não paralelizar pequenos corpos de Loop

A paralelização de corpos de loop relativamente pequeno pode causar o associado agendamento sobrecarga para superar os benefícios de processamento paralelo. Considere o exemplo a seguir, 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 do loop paralelo é pequena demais para se beneficiar com a sobrecarga de processamento paralelo. Você pode melhorar o desempenho, este loop realizando mais trabalho no corpo do loop ou realizando o loop serialmente.

go to top

Expressar o paralelismo no nível mais alto possível

Quando você paralelizar código somente no nível inferior, você pode introduzir uma construção de bifurcação-junção não dimensiona como o aumento do número de processadores. A bifurcação-junção construção é uma construção onde uma tarefa divide o seu trabalho menores paralelas subtarefas e aguarda a essas subtarefas concluir. Cada subtarefa pode dividir recursivamente próprio em subtarefas adicionais.

Embora o modelo de bifurcação-junção pode ser útil para solucionar vários problemas, há situações em que a sobrecarga de sincronização pode diminuir a escalabilidade. Por exemplo, considere o seguinte código serial que processa os 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 do loop é independente, paralelizar grande parte do trabalho, conforme mostrado no exemplo a seguir. Este exemplo usa a Concurrency::parallel_for o algoritmo em paralelo 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 uma construção de bifurcação-junção chamando o ProcessImage função em um loop. Cada chamada para ProcessImage não retorna até terminar cada subtarefa.

// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
   for_each(bitmaps.begin(), bitmaps.end(), [&f](Bitmap* bmp) {
      ProcessImage(bmp, f);
   });
}

Se cada iteração do loop paralelo ou executa quase nenhum trabalho ou o trabalho realizado pelo loop paralelo é imbalanced, ou seja, algumas iterações do loop demoram mais do que outros, o agendamento sobrecarga que é necessário para freqüentemente bifurcação e associação trabalho pode pesar sobre os benefícios para execução em paralelo. Essa sobrecarga aumenta à medida que o aumento do número de processadores.

Para reduzir a quantidade de agendamento de sobrecarga, neste exemplo, você pode paralelizar loops externas antes de paralelizar loops internos ou usar outra construção paralela, como o pipelining. O exemplo seguinte modifica a ProcessImages função use o Concurrency::parallel_for_each o algoritmo em paralelo o loop externo.

// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
   parallel_for_each(bitmaps.begin(), bitmaps.end(), [&f](Bitmap* bmp) {
      ProcessImage(bmp, f);
   });
}

Para obter um exemplo semelhante que usa um pipeline para executar o processamento de imagens em paralelo, consulte Demonstra Passo a passo: A criação de uma rede de processamento de imagens.

go to top

Use o parallel_invoke para solucionar problemas de "dividir e conquistar"

A "dividir e conquistar" problema é uma forma da construção de bifurcação-junção usa recursão para dividir uma tarefa em subtarefas. Além de Concurrency::task_group e Concurrency::structured_task_group classes, você também pode usar o Concurrency::parallel_invoke o algoritmo para resolver problemas de "dividir e conquistar". O parallel_invoke algoritmo tem uma sintaxe mais sucinta que objetos de grupo de tarefas e é útil quando você tem um número fixo de tarefas em paralelo.

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

// 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 parallel_invoke algoritmo realiza a última da série de tarefas no contexto de chamada.

Para obter a versão completa deste exemplo, consulte Como: Use o parallel_invoke para escrever uma rotina de classificação paralela. Para obter mais informações sobre o parallel_invoke o algoritmo, consulte Algoritmos paralelos.

go to top

Use o cancelamento ou o tratamento de exceção para quebra de um Loop paralelo

A PPL fornece duas maneiras para cancelar o trabalho paralelo é realizado por um grupo de tarefas ou o algoritmo paralelo. Uma maneira é usar o mecanismo de cancelamento fornecida pelo Concurrency::task_group e Concurrency::structured_task_group classes. A outra forma é lançar uma exceção no corpo de uma função de trabalho da tarefa. O mecanismo de cancelamento é mais eficiente do que em uma árvore de trabalho paralelos de cancelamento de manipulação de exceção. A árvore de trabalho paralelos é um grupo de grupos de tarefa relacionada na qual alguns grupos de tarefas contenham outros grupos de tarefas. O mecanismo de cancelamento cancela a um grupo de tarefas e de seus grupos de tarefas do filho de uma maneira de cima para baixo. Por outro lado, a manipulação de exceção funciona de uma maneira de baixo para cima e deve cancelar a cada grupo de tarefas filho independentemente como a exceção se propaga para cima.

Quando você trabalha diretamente com um objeto de grupo de tarefas, use o Concurrency::task_group::cancel ou Concurrency::structured_task_group::cancel métodos para cancelar o trabalho que pertence a esse grupo de tarefas. Para cancelar um algoritmo paralelo, por exemplo, parallel_for, crie um grupo de tarefas do pai e cancelar esse grupo de tarefas. Por exemplo, considere a seguinte função, parallel_find_any, que procura por 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;

   // Use parallel_for to search for the element. 
   // The task group enables a work function to cancel the overall 
   // operation when it finds the result.

   structured_task_group tasks;
   tasks.run_and_wait([&]
   {
      parallel_for(std::size_t(0), count, [&](int n) {
         if (a[n] == what)
         {
            // Set the return value and cancel the remaining tasks. 
            position = n;            
            tasks.cancel();
         }
      });
   });

   return position;
}

Como os algoritmos paralelos usam grupos de tarefas quando uma das iterações paralelas cancela o grupo de tarefas do pai, a tarefa geral será cancelada. Para obter a versão completa deste exemplo, consulte Como: Use o cancelamento para quebra de um Loop paralelo.

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

// 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(_children.begin(), _children.end(), [&](tree& child) {
      child.for_all(action);
   });

   // Perform the action on this node.
   action(*this);
}

O chamador da tree::for_all método pode lançar uma exceção se ele não requer a função de trabalho a ser chamado em cada elemento da árvore. A exemplo a seguir mostra a search_for_value a função, que procura um valor em fornecida tree objeto. O search_for_value função usa uma função de trabalho que lança uma exceção quando o elemento atual da árvore corresponde ao valor fornecido. O search_for_value função usa um try-catch bloco 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, consulte Como: Use o tratamento de exceção para quebra de um Loop paralelo.

Para obter informações gerais sobre o cancelamento e mecanismos de manipulação de exceção são fornecidos da PPL, consulte Cancelamento na PPL e O Runtime de simultaneidade de manipulação de exceção.

go to top

Compreender como o cancelamento e a manipulação de exceção afetam a destruição de objeto

Em uma árvore de trabalho paralelo, uma tarefa que seja cancelada impede tarefas filho em execução. Isso pode causar problemas se uma das tarefas filho realiza uma operação que é importante para seu aplicativo, como, por exemplo, liberando um recurso. Além disso, o cancelamento da tarefa pode causar uma exceção propagar através de um destruidor de objeto e causar um comportamento indefinido em seu aplicativo.

No exemplo a seguir, o Resource classe descreve um recurso e o Container classe descreve um recipiente que contém recursos. Em seu destruidor, o Container chamadas de classe a cleanup método em dois dos seus Resource membros em paralelo e, em seguida, chama o cleanup método em sua terceira Resource membro.

// 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 tem problemas por conta própria, considere o seguinte código que executa duas tarefas em paralelo. A primeira tarefa cria um Container objeto e a segunda tarefa cancela a tarefa geral. Como ilustração, o exemplo usa dois Concurrency::event objetos para certificar-se de que o cancelamento ocorre após a Container objeto é criado e que o Container objeto é 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 produz a seguinte saída.

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

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

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

  • O cancelamento da tarefa pai faz com que a tarefa de filho para lançar uma exceção interna. Porque o Container destruidor não tratar essa exceção, a exceção é propagada para cima e o terceiro recurso não é liberado.

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

Recomendamos que você não realizar operações críticas, como, por exemplo, a liberação de recursos, em tarefas, a menos que você pode garantir que essas tarefas não serão canceladas. Também recomendamos que você não use a funcionalidade de tempo de execução que pode lançar no destruidor de seus tipos.

go to top

Não bloquear repetidamente em um Loop paralelo

Um loop em paralelo, como Concurrency::parallel_for ou Concurrency::parallel_for_each que é dominado pelo bloqueio de operações podem fazer com que o tempo de execução criar vários segmentos em um curto período de tempo.

O Runtime de simultaneidade realiza o trabalho adicional quando uma tarefa é concluída ou cooperativamente bloqueia ou produz. Quando um paralelo de blocos de iteração de loop, o runtime pode começar outra iteração. Quando não há nenhum thread ocioso disponível, o runtime cria um novo segmento.

Quando o corpo de um paralelo loop, ocasionalmente, blocos, este mecanismo ajuda a maximizar a produtividade geral da tarefa. No entanto, ao bloqueiam a muitas iterações, o runtime pode criar muitos segmentos 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 chama o Concurrency::send função em cada iteração de um parallel_for loop. Porque send bloqueia de forma cooperativa, o runtime cria um novo segmento para executar o trabalho adicional sempre 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);
   });
}

Recomendamos que você refatora seu código para evitar esse padrão. Neste exemplo, você pode evitar a criação de threads adicionais chamando send em uma série for loop.

go to top

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

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

Quando uma tarefa executa uma operação de bloqueio, o runtime pode executar outro trabalho enquanto a primeira tarefa aguarda a dados. Quando o agendamento no modo de usuário (UMS) estiver ativada, o runtime outro trabalho quando executa uma tarefa executa um cooperativo bloqueando a operação ou uma operação de bloqueio que envolve uma transição de kernel. Quando normal do segmento de agendamento, que é o padrão, é ativada, o runtime realiza outro trabalho somente quando uma tarefa executa um operação de bloqueio de cooperativo. O runtime reagenda a tarefa de espera quando ele desbloqueia. Normalmente, o runtime reagenda tarefas que foram desbloqueadas mais recentemente antes de ele reagenda tarefas que foram desbloqueadas menos recentemente. Portanto, o runtime foi possível agendar o trabalho desnecessário durante a operação de bloqueio, o que leva à redução do desempenho. Da mesma forma, quando você 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 executar o trabalho desnecessário.

Considere o exemplo a seguir define o parallel_find_answer a função, que pesquisa um elemento da matriz fornecida que satisfaça a função de predicado fornecido. Quando a função de predicado retorna true, a função de trabalho paralela cria um Answer de objeto 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 new operador executa uma alocação de heap, que pode bloquear. Quando o agendamento no modo de usuário (UMS) estiver ativada, o tempo de execução executa outro trabalho durante a operação de bloqueio. Quando o agendamento de thread normal é ativado, o tempo de execução executa outro trabalho, somente quando a tarefa executa um cooperativo de bloqueio de chamada, como, por exemplo, uma chamada para Concurrency::critical_section::lock.

O exemplo a seguir mostra como impedir que o trabalho desnecessário e, assim, melhorar o desempenho. Este exemplo cancela o grupo de tarefas antes de ele aloca o armazenamento para o Answer objeto.

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

go to top

Não gravar os dados compartilhados em um Loop paralelo

O Runtime de simultaneidade fornece diversas estruturas de dados, por exemplo, Concurrency::critical_section, que sincronizar o acesso simultâneo aos dados compartilhados. Essas estruturas de dados são úteis em muitos casos, por exemplo, quando várias tarefas com pouca freqüência exigem acesso compartilhado a um recurso.

Considere o exemplo a seguir usa a Concurrency::parallel_for_each algoritmo e uma critical_section o objeto para calcular a contagem dos números primos um std::array objeto. Este exemplo não é dimensionado, porque cada segmento deve esperar para acessar a variável compartilhada prime_sum.

critical_section cs;
prime_sum = 0;
parallel_for_each(a.begin(), a.end(), [&](int i) {
   cs.lock();
   prime_sum += (is_prime(i) ? i : 0);
   cs.unlock();
});

Este exemplo também pode levar a mau desempenho porque a operação de bloqueio freqüente efetivamente serializa o loop. Além disso, quando um objeto de Runtime de simultaneidade realiza uma operação de bloqueio, o Agendador pode criar um segmento adicional para executar outro trabalho enquanto o primeiro thread aguarda a dados. Se o runtime cria muitos segmentos porque muitas tarefas que estão aguardando para dados compartilhados, o aplicativo pode insatisfatório ou entrar em um estado de recursos baixos.

A PPL define o Concurrency::combinable classe, que ajuda a eliminar o estado compartilhado, fornecendo acesso a recursos compartilhados de forma livre de bloqueio. O combinable classe fornece armazenamento thread local, que permite executar cálculos refinados e mescle esses cálculos em um resultado final. Você pode pensar em um combinable o objeto como uma variável de redução.

O exemplo seguinte modifica aquela anterior usando um combinable de objeto em vez de um critical_section o objeto para calcular a soma. Este exemplo dimensiona porque cada segmento possui sua própria cópia local da soma. Este exemplo usa a Concurrency::combinable::combine método para mesclar as computações locais com o resultado final.

combinable<int> sum;
parallel_for_each(a.begin(), a.end(), [&](int i) {
   sum.local() += (is_prime(i) ? i : 0);
});
prime_sum = sum.combine(plus<int>());

Para obter a versão completa deste exemplo, consulte Como: Podem ser combinado para melhorar o desempenho do uso. For more information about the combinable class, see Paralelo recipientes e objetos.

go to top

Quando possível, evitar o falso compartilhamento

Falso compartilhamento ocorre quando a gravação de várias tarefas simultâneas que estão sendo executados em processadores separados para variáveis que estão localizadas na mesma linha de cache. Quando uma tarefa grava uma das variáveis, a linha de cache para ambas as variáveis é invalidada. Cada processador deve recarregar a linha de cache sempre que a linha de cache é invalidada. Portanto, o falso compartilhamento pode causar uma redução no desempenho em seu aplicativo.

Exemplo básico mostra duas tarefas a seguir simultâneas que cada 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, você pode modificar o exemplo de duas variáveis de contador. Este exemplo calcula o valor do contador final após o término de tarefas. No entanto, este exemplo ilustra o falso compartilhamento porque as variáveis count1 e count2 provavelmente devem estar 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 para eliminar o falso compartilhamento é certificar-se de que as variáveis de contador estão nas linhas de cache separadas. O exemplo seguinte alinha as variáveis count1 e count2 em 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 assume que o tamanho do cache de memória é de bytes de 64 ou menos.

Recomendamos que você use o Concurrency::combinable classe quando você deve compartilhar dados entre tarefas. O combinable classe cria variáveis de segmento locais de tal forma que o falso compartilhamento é menos provável. For more information about the combinable class, see Paralelo recipientes e objetos.

go to top

Certifique-se de que as variáveis são válidas em todo o tempo de vida de uma tarefa

Quando você fornece uma expressão lambda para um grupo de tarefas ou o algoritmo paralelo, a cláusula de captura Especifica se o corpo da expressão lambda acessa variáveis no escopo de fechamento por valor ou referência. Quando você passar variáveis para uma expressão lambda por referência, você deve garantir que o tempo de vida dessa variável persiste até o término da tarefa.

Considere o exemplo a seguir define o object classe e o perform_action função. O perform_action função cria uma object variável e executa alguma ação nessa variável de forma assíncrona. Porque a tarefa não é garantida para terminar antes de perform_action função retorna o programa irá travar ou exibem o comportamento não especificado, se a object variável é 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 seu aplicativo, você pode usar uma das seguintes técnicas para garantir que as variáveis permanecem válidas em todo o tempo de vida de cada tarefa.

O exemplo seguinte passa a object variável pelo valor para a tarefa. Portanto, a tarefa opera em sua 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();
   });
}

Porque o object variável é passada por valor, as alterações de estado que ocorrerem a essa variável não aparecem na cópia original.

O exemplo a seguir usa a Concurrency::task_group::wait método para certificar-se de que a tarefa termina antes do perform_action retorna a função.

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

Porque a tarefa agora termina antes que a função retorna, o perform_action função não se comporta assincronamente.

O exemplo seguinte modifica a perform_action função seja uma referência para o object variável. O chamador deve garantir que a vida útil do object variável é válida até terminar a tarefa.

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

Você também pode usar um ponteiro para controlar o tempo de vida de um objeto que você passa para um grupo de tarefas ou o algoritmo paralelo.

Para obter mais informações sobre expressões lambda, consulte Lambda Expressions in C++.

go to top

Consulte também

Tarefas

Como: Use o parallel_invoke para escrever uma rotina de classificação paralela

Como: Use o cancelamento para quebra de um Loop paralelo

Como: Podem ser combinado para melhorar o desempenho do uso

Conceitos

As práticas recomendadas de Runtime de simultaneidade

Biblioteca paralela de padrões (PPL)

Paralelo recipientes e objetos

Algoritmos paralelos

Cancelamento na PPL

O Runtime de simultaneidade de manipulação de exceção

Outros recursos

Demonstra Passo a passo: A criação de uma rede de processamento de imagens

Práticas recomendadas para a biblioteca de agentes assíncronos

Práticas recomendadas de gerais no Runtime de simultaneidade

Histórico de alterações

Date

History

Motivo

Março de 2011

Adicionadas informações sobre como evitar repetidas de bloqueio em um loop em paralelo e como o cancelamento e a manipulação de exceção afetam a destruição de objeto.

Aprimoramento de informações.

Maio de 2010

Diretrizes expandidas.

Aprimoramento de informações.