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:
Use parallel_invoke para resolver problemas de dividir e conquistar
Usar cancelamento ou tratamento de exceção para quebrar de um loop paralelo
Entenda como o cancelamento e o tratamento de exceções afetam a destruição de objetos
Não execute operações de bloqueio ao cancelar o trabalho paralelo
Verifique se as variáveis são válidas durante todo o tempo de vida de uma tarefa
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.
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.
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.
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.
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.
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
.
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;
}
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.
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.
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.
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