Práticas recomendadas na Biblioteca de Padrões Paralelos
Este documento descreve como fazer o melhor uso mais eficiente de paralela da biblioteca (PPL). O PPL fornece contêineres, objetos, e algoritmos de uso geral para executar o paralelismo refinado.
Para obter mais informações sobre o PPL, consulte Biblioteca de padrões paralelos (PPL).
Seções
Este documento contém as seções a seguir:
Não Paralelizar Corpos de Loop Pequenos
Expressar Paralelismo no Nível Mais Alto Possível
Usar parallel_invoke para Resolver Problemas Dividir e Conquistar
Usar Cancelamento ou Tratamento de Exceção para Parar um Loop Paralelo
Compreender como Cancelamento e Tratamento de Exceção Afetam a Destruição do Objeto
Não Bloquear Repetidamente em um Loop Paralelo
Não Realizar Operações de Bloqueio ao Cancelar o Trabalho Paralelo
Não Gravar em Dados Compartilhados em um Loop Paralelo
Quando Possível, Evitar Compartilhamento Falso
Verificar se as Variáveis são Válidas Durante o Tempo de Vida de uma Tarefa
Não Paralelizar Corpos de Loop Pequenos
O parallelization de corpos de loop relativamente pequenos pode fazer com que a sobrecarga de programação associada aumente 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 do loop paralela é muito pequena se beneficiar de sobrecarga para o processamento paralelo. Você pode melhorar o desempenho desse loop executando mais trabalho no corpo de loop ou executando o loop em série.
[Superior]
Expressar Paralelismo no Nível Mais Alto Possível
Quando você parallelize somente o código no de nível baixo, você pode enviar uma compilação de forquilha- junção não dimensão à medida que o número de processadores aumenta. Uma compilação de forquilha- junção é uma construção onde uma tarefa divida seu trabalho em subtarefas paralelas menores e aguarde essas subtarefas para concluir. Cada subtarefa recursivamente pode ser dividida em subtarefas adicionais.
Embora o modelo de forquilha- junção pode 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 seguinte código em série 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 do loop é independente, você pode parallelize muito de trabalho, conforme mostrado no exemplo a seguir. Este exemplo usa o algoritmo de concurrency::parallel_for para parallelize 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 compilação de forquilha- junção chamando a função de ProcessImage em um loop. Cada chamada para ProcessImage não retorna 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 ou não executa praticamente qualquer trabalho, ou o trabalho executado pelo loop paralelo é desequilibrado, ou seja, algumas interações do loop leva mais tempo do que outros, a sobrecarga de programação que é necessária para se bifurcar com frequência e o trabalho de junção podem aumentar o benefício a execução paralela. Essa sobrecarga aumenta à medida que o número de processadores aumenta.
Para reduzir a quantidade de sobrecarga de programação neste exemplo, você pode parallelize loop externas antes que você parallelize loops internos ou use outra construção paralela como indicação do. O exemplo a seguir altera a função de ProcessImages para usar o algoritmo de concurrency::parallel_for_each para parallelize 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 use um pipeline para executar o processamento paralelo de imagem, consulte Instruções passo a passo: criando uma rede de processamento de imagem.
[Superior]
Usar parallel_invoke para Resolver Problemas Dividir e Conquistar
Um problema de partilha-e- conquista é uma forma de criação de forquilha- junção que usa a recursão para interromper uma tarefa em subtarefas. Além das classes de concurrency::task_group e de concurrency::structured_task_group , você também pode usar o algoritmo de concurrency::parallel_invoke para resolver partilha-e- conquista problemas. O algoritmo de parallel_invoke tem uma sintaxe mais sucinto de objetos do grupo de trabalho, e é útil quando você tem um número fixo de tarefas paralelas.
O exemplo a seguir ilustra o uso do algoritmo de parallel_invoke implementação do 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 algoritmo de parallel_invoke executa o último da série de tarefas no contexto de chamada.
Para a versão completo deste exemplo, consulte Como usar parallel_invoke para escrever uma rotina de classificação em paralelo. Para obter mais informações sobre o algoritmo de parallel_invoke , consulte Algoritmos paralelos.
[Superior]
Usar Cancelamento ou Tratamento de Exceção para Parar um Loop Paralelo
O PPL fornece duas maneiras de cancelar o trabalho paralelo que é executado por um grupo de trabalho ou por um algoritmo paralelo. Um modo é usar o mecanismo de cancelamento que é fornecido pela classes de concurrency::task_group e de concurrency::structured_task_group . Outra maneira é gerar uma exceção no corpo de uma função de trabalho da tarefa. O mecanismo de cancelamento é mais eficiente do que a manipulação de exceção em cancelar uma árvore de trabalho paralelo. Uma árvore paralela de trabalho é um grupo de grupos de trabalho relacionados em que alguns grupos de trabalho contêm outros grupos de trabalho. O mecanismo de cancelamento cancela um grupo de trabalho e seus grupos de trabalho filho de uma maneira de reversão. Por outro lado, o trabalho de manipulação de exceção em uma forma de baixo para cima e devem cancelar cada grupo de trabalho filho independente como as propagações de exceção para cima.
Quando você trabalha diretamente a um objeto de grupo de trabalho, use os métodos de concurrency::task_group::cancel ou de concurrency::structured_task_group::cancel para cancelar o trabalho que pertence ao grupo de trabalho. Para cancelar um algoritmo paralelo, por exemplo, parallel_for, cria um grupo de trabalho e cancelar um pai que grupo de trabalho. Por exemplo, considere a seguinte função, parallel_find_any, que pesquisa 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;
// 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 trabalho, quando uma das iterações paralelas cancela o grupo de trabalho pai, a tarefa total será cancelada. Para a versão completo deste exemplo, consulte Como usar cancelamento para parar um loop paralelo.
Embora a manipulação de exceção é o modo mais eficiente para cancelar o trabalho paralelo do mecanismo de cancelamento, há casos em que a manipulação de exceção é apropriado. Por exemplo, o seguinte método, for_all, executa recursivamente uma função de trabalho em cada nó de uma estrutura de tree . Neste exemplo, o membro de dados de _children é std::list que contém objetos de 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 de tree::for_all pode gerar uma exceção se não precisar da função de trabalho ser chamada cada elemento da árvore. O exemplo a seguir mostra a função de search_for_value , que procura um valor em tree fornecido objeto. A função de search_for_value usa uma função de trabalho que gerencie uma exceção quando o elemento atual da árvore corresponde ao valor fornecido. A função de search_for_value usa um bloco de try-catch para capturar a exceção e para 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 a versão completo deste exemplo, consulte Como usar tratamento de exceções para parar um loop paralelo.
Para obter mais informações gerais sobre os mecanismos de cancelamento e manipulação de exceções gerais que são fornecidos por PPL, consulte Cancelamento no PPL e Tratamento de exceções no tempo de execução de simultaneidade.
[Superior]
Compreender como Cancelamento e Tratamento de Exceção Afetam a Destruição do Objeto
Em uma árvore de trabalho paralelo, uma tarefa que é cancelada evita tarefas filhos de execução. Isso pode causar problemas se uma das tarefas filhas executa uma operação que é importante para seu aplicativo, como liberar um recurso. Além disso, o cancelamento da tarefa pode gerar uma exceção se propague por meio de um destruidor do objeto e provocar comportamento indefinido em seu aplicativo.
No exemplo a seguir, a classe de Resource descreve um recurso e a classe de Container descreve um contêiner que contém recursos. No destruidor, a classe de Container chama o método de cleanup em dois dos membros de Resource em paralelo e chame o método de cleanup no terceiro membro de 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 tem nenhum problema sozinho, considere o seguinte código que executa duas tarefas em paralelo. A primeira tarefa cria um objeto de Container e a segunda tarefa cancela a tarefa total. Para fins ilustrativos, o exemplo usa dois objetos de concurrency::event para garantir que o cancelamento ocorre depois que o objeto de Container é criado e que o objeto de Container será destruído depois que a operação de cancelamento ocorre.
// 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;
}
Este exemplo gera a seguinte saída:
Este exemplo de código contém os seguintes problemas que podem fazer com que se com comportamento de modo diferente do esperado:
O cancelamento da tarefa pai faz a tarefa filho, a chamada para concurrency::parallel_invoke, também ser cancelado. Consequentemente, esses dois recursos não são liberados.
O cancelamento da tarefa pai faz a tarefa filho gerar uma exceção interna. Como o destruidor de Container não trata esta exceção, a exceção será propagada para cima e o terceiro recurso não é liberado.
A exceção gerada pela tarefa filho propaga pelo destruidor de Container . Lançar de um destruidor coloca o aplicativo em um estado indeterminado.
Recomendamos que você não executa operações críticas, como se liberar recursos, tarefas a menos que você possa garantir que essas tarefas não serão canceladas. Recomendamos também não usar a funcionalidade de tempo de execução que pode lançar no destruidor de seus tipos.
[Superior]
Não Bloquear Repetidamente em um Loop Paralelo
Um loop paralelo como concurrency::parallel_for ou concurrency::parallel_for_each que é dominado bloqueando operações pode fazer com que o tempo de execução crie muitos threads em um curto período de tempo.
O tempo de execução de simultaneidade executa o trabalho adicional quando uma tarefa é concluída ou cooperativa blocos ou gera. Quando os blocos paralelos de uma iteração do loop, o tempo de execução podem iniciar outra iteração. Quando não houver threads ocioso disponível, o tempo de execução cria um novo thread.
Quando o corpo de blocos paralelos de um loop ocasionalmente, esse mecanismo para ajudar a maximizar a taxa de transferência total da tarefa. No entanto, quando o bloco de muitas interações, o tempo de execução pode criar vários threads para executar o trabalho adicional. Isso poderia gerar condições de memória baixa ou fraca para a utilização de recursos de hardware.
Considere o exemplo seguinte que chama a função de concurrency::send em cada iteração do loop de parallel_for . Como os blocos de send cooperativa, o tempo de execução para criar um novo thread para executar o trabalho adicional cada 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);
});
}
Recomendamos que você refactor seu código evitar esse padrão. Neste exemplo, você pode impedir a criação de threads adicionais chamando send em um loop em série de for .
[Superior]
Não Realizar 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 de concurrency::task_group::cancel ou de concurrency::structured_task_group::cancel para cancelar o trabalho paralelo.
Quando uma tarefa executa uma operação cooperativa de bloqueio, o tempo de execução pode executar outro trabalho quando a primeira tarefa está esperando dados. O tempo de execução reagendar a tarefa de espera quando desbloqueado. O tempo de execução reagendar normalmente as tarefas que foram desbloqueadas mais recentemente antes de reprogramem as tarefas que foram desbloqueadas menos recentemente. Consequentemente, o tempo de execução pode agendar trabalho desnecessário durante a operação de bloqueio, o que resulta em desempenho reduzido. Da mesma forma, quando você executa uma operação de bloqueio antes que você cancela o trabalho paralelo, a operação de bloqueio pode retardar a chamada a cancel. Isso causa outras tarefas executar trabalho desnecessário.
Considere o exemplo que define a função de parallel_find_answer , que procura um elemento da matriz fornecida que satisfaz a função fornecida de predicado. Quando a função de predicado retornar true, a função de trabalho paralela cria um objeto de Answer e cancelar a tarefa total.
// 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 de new executa uma alocação de heap, que pode bloquear. O tempo de execução executa o outro trabalho somente quando a tarefa executa uma chamada cooperativo de bloqueio, como uma chamada a concurrency::critical_section::lock.
O exemplo a seguir mostra como evitar o trabalho desnecessário, e melhora assim o desempenho. Este exemplo cancela o grupo de trabalho antes de atribuir o armazenamento do objeto de 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;
}
[Superior]
Não Gravar em Dados Compartilhados em um Loop Paralelo
O tempo de execução de simultaneidade o 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 várias tarefas raramente requerem acesso compartilhado a um recurso.
Considere o exemplo que usa o algoritmo de concurrency::parallel_for_each e um objeto de critical_section para calcular a contagem de números à esquerda em um objeto de std::array . Este exemplo não dimensiona porque cada thread deve esperar para acessar prime_sumvariável compartilhado.
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();
});
Este exemplo também pode resultar em um baixo desempenho porque a operação frequente de bloqueio serializa efetivamente o loop. Além disso, quando um objeto de tempo de execução de simultaneidade executa uma operação de bloqueio, o agendador pode criar um thread adicional para executar outro trabalho quando o primeiro thread está esperando dados. Se o tempo de execução cria muitos threads como muitas tarefas estão aguardando dados compartilhados, o aplicativo pode ser realizadas ou entrar em um estado de baixo recurso.
O PPL define a classe de concurrency::combinable , que ajuda você eliminam o estado compartilhado fornecendo acesso a recursos compartilhados em um modo lock-partition-id> livre. A classe de combinable fornece o armazenamento de thread local que permite executar computações refinados e mesclar essas computações em um resultado final. Você pode pensar em um objeto de combinable como uma variável de redução.
O exemplo a seguir altera anterior usando um objeto de combinable em vez de um objeto de critical_section para calcular a soma. Escalas desse exemplo porque cada thread mantém sua própria cópia local da soma. Este exemplo usa o método de concurrency::combinable::combine para mesclar as computações 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 a versão completo deste exemplo, consulte Como usar combinável para melhorar o desempenho. Para obter mais informações sobre a classe combinable, consulte Contêineres e objetos em paralelo.
[Superior]
Quando Possível, Evitar Compartilhamento Falso
Compartilhar false ocorre quando várias tarefas simultâneas que estão sendo executadas em processadores separados gravam em variáveis que estão localizados na mesma linha de cache. Quando uma tarefa grava a uma das variáveis, a linha de cache para ambas as variáveis é invalidado. Cada processador deverá recarregar a linha de cache sempre que a linha do cache é invalidado. Consequentemente, compartilhar false podem fazer com que o desempenho reduzido em seu aplicativo.
O exemplo básico mostra duas tarefas simultâneas que cada incremento uma variável vez compartilhado.
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 alterar o exemplo para usar duas variáveis do contador. Esse exemplo calcula o valor do contador final depois que as tarefas é concluído. No entanto, este exemplo ilustra compartilhar false como as variáveis count1 e count2 provavelmente serão localizados 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 compartilhar eliminar false é garantir que as variáveis de contador estão em linhas separadas do cache. O exemplo a seguir 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 supõe que o tamanho do cachê de memória é 64 ou menos bytes.
Recomendamos que você use a classe de concurrency::combinable quando você deve compartilhar dados entre tarefas. A classe de combinable cria variáveis de thread local de forma que o compartilhamento false é menos provável. Para obter mais informações sobre a classe combinable, consulte Contêineres e objetos em paralelo.
[Superior]
Verificar se as Variáveis são Válidas Durante o Tempo de Vida de uma Tarefa
Quando você fornece uma expressão de lambda a um grupo de trabalho ou um algoritmo paralelo, a cláusula de captura especifica se o corpo de variáveis dos acessos da expressão de lambda no escopo incluindo pelo valor ou por referência. Quando você passa as variáveis a uma expressão lambda por referência, você deve garantir que o tempo de vida da variável persiste até que a tarefa seja concluída.
Considere o exemplo seguinte que define a classe de object e a função de perform_action . A função de perform_action cria uma variável de object e executa uma ação nessa variável de forma assíncrona. Como a tarefa não é garantida ser concluída antes que a função de perform_action retorna, o programa falhará ou exibirá o comportamento não especificado se a variável de object é destruído na tarefa está 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 de seu aplicativo, você pode usar uma das seguintes técnicas para garantir que as variáveis permanecem válidos durante todo o tempo de vida de cada tarefa.
O exemplo a seguir transmite a variável de object pelo valor para a tarefa. Consequentemente, 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 a variável de object é passado pelo valor, todas as alterações de estado que ocorrem a essa variável não aparecem na cópia original.
O exemplo a seguir usa o método de concurrency::task_group::wait para garantir que os conclusão da tarefa antes da função de perform_action retornam.
// 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 é concluída agora antes que a função retorna, a função de perform_action não se comporta de forma assíncrona.
O exemplo a seguir altera a função de perform_action para executar uma referência à variável de object . O chamador deve garantir que o tempo de vida da variável de object é válido até que a tarefa seja concluída.
// 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ê transmita a um grupo de trabalho ou um algoritmo paralelo.
Para obter mais informações sobre expressões lambda, consulte Expressões lambda em C++.
[Superior]
Consulte também
Tarefas
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 parar um loop paralelo
Como usar combinável para melhorar o desempenho
Conceitos
Biblioteca de padrões paralelos (PPL)
Contêineres e objetos em paralelo
Tratamento de exceções no tempo de execução de simultaneidade
Práticas recomendadas na Biblioteca de Agentes Assíncrona
Práticas recomendadas gerais no Tempo de Execução de Simultaneidade
Outros recursos
Práticas recomendadas do Tempo de Execução de Simultaneidade