Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Este documento descreve a melhor forma de fazer uso efetivo da Biblioteca de Padrões Paralelos (PPL). O PPL fornece contêineres, objetos e algoritmos de uso geral para executar paralelismo refinado.
Para obter mais informações sobre o PPL, consulte Biblioteca de padrões paralelos (PPL).
Secções
Este documento contém as seguintes secções:
Use parallel_invoke para resolver problemas de divisão e conquista
Usar cancelamento ou tratamento de exceção para interromper um loop paralelo
Compreender 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
Certifique-se de que as variáveis são válidas durante toda a vida útil de uma tarefa
Não paralelizar pequenos blocos de código
A paralelização de corpos de loop relativamente pequenos 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 de cada iteração de um loop paralelo é tão pequena que não compensa o esforço necessário para o processamento paralelo. Você pode melhorar o desempenho desse loop executando mais trabalho no corpo do loop ou executando o loop em série.
[Topo]
Expresse o paralelismo ao mais alto nível possível
Quando se paraleliza o código apenas ao nível mais baixo, pode-se criar uma construção fork-join que não escala à medida que o número de processadores aumenta. Uma estrutura fork-join é uma em que uma tarefa divide o seu trabalho em subtarefas paralelas menores e espera que essas subtarefas sejam concluídas. Cada subtarefa pode dividir-se 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 seguinte código serial 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, você pode 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 uma estrutura fork-join chamando a função ProcessImage num ciclo. 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 não executa quase nenhum trabalho, ou o trabalho que é executado pelo loop paralelo é desequilibrado, ou seja, algumas iterações de loop levam mais tempo do que outras, a sobrecarga de agendamento necessária para bifurcar e unir trabalho com frequência pode compensar 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, pode-se paralelizar os loops externos antes de paralelizar os loops internos ou usar outra construção paralela, como o pipeline. O exemplo a seguir modifica a função ProcessImages para 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 imagem em paralelo, consulte Passo a passo: Criando uma rede Image-Processing.
[Topo]
Use parallel_invoke para resolver problemas de dividir e conquistar
Um problema de divisão e conquista é uma forma da construção fork-join que usa recursão para dividir uma tarefa em subtarefas. Além das classes concurrency::task_group e concurrency::structured_task_group , você também pode usar o algoritmo concurrency::p arallel_invoke para resolver problemas de divisão e conquista. O parallel_invoke algoritmo tem uma sintaxe mais sucinta do que os objetos do 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 parallel_invoke algoritmo executa a última da série de tarefas no contexto de chamada.
Para obter a versão completa deste exemplo, consulte Como usar parallel_invoke para escrever uma rotina de classificação paralela. Para obter mais informações sobre o parallel_invoke algoritmo, consulte Algoritmos paralelos.
[Topo]
Usar cancelamento ou tratamento de exceção para interromper um loop paralelo
O PPL fornece duas maneiras de cancelar o trabalho paralelo que é executado por um grupo de tarefas ou algoritmo paralelo. Uma maneira é usar o mecanismo de cancelamento que é fornecido pelas classes concurrency::task_group e concurrency::structured_task_group . A outra maneira é lançar uma exceção no corpo de uma função de tarefa. O mecanismo de cancelamento é mais eficiente do que o tratamento de exceções no cancelamento de uma árvore de trabalho paralelo. Uma árvore de trabalho paralela é um grupo de grupos de tarefas relacionados no qual alguns grupos de tarefas contêm outros grupos de tarefas. O mecanismo de cancelamento cancela um grupo de tarefas e os seus grupos de tarefas subordinados de cima para baixo. Por outro lado, o tratamento de exceções funciona de baixo para cima e deve cancelar cada grupo de tarefas filho de forma independente à medida que a exceção se propaga para cima na hierarquia.
Quando você trabalha 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 a esse grupo de tarefas. Para cancelar um algoritmo paralelo, por exemplo, parallel_for, crie um grupo de tarefas pai e cancele esse grupo de tarefas. Por exemplo, considere a seguinte função, parallel_find_any, que procura 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, quando uma das iterações paralelas cancela o grupo de tarefas pai, a tarefa geral é cancelada. Para obter a versão completa deste exemplo, consulte Como usar o cancelamento para interromper um loop paralelo.
Embora o tratamento de exceções seja uma maneira menos eficiente de cancelar o trabalho paralelo do que o mecanismo de cancelamento, há casos em que o tratamento de exceções é apropriado. Por exemplo, o seguinte método, for_all, executa de forma recursiva uma função operacional em cada nó de uma tree estrutura. Neste exemplo, o _children 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(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 não haja necessidade de a função de trabalho ser chamada em cada elemento da árvore. O exemplo a seguir mostra a search_for_value função, que procura um valor no objeto fornecido tree . A 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. A 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 usar o tratamento de exceções para interromper um loop paralelo.
Para obter mais informações gerais sobre os mecanismos de cancelamento e tratamento de exceções fornecidos pelo PPL, consulte Cancelamento no PPL e Tratamento de exceções.
[Topo]
Compreender como o cancelamento e o tratamento de exceções afetam a destruição de objetos
Em uma árvore de trabalho paralelo, uma tarefa cancelada impede a execução de tarefas filhas. Isso pode causar problemas se uma das tarefas subordinadas executar uma operação que é importante para a sua aplicação, como libertar um recurso. Além disso, o cancelamento de tarefas pode fazer com que uma exceção se propague por meio de um destruidor de objeto e cause um comportamento indefinido em seu aplicativo.
No exemplo a seguir, a Resource classe descreve um recurso e a Container classe descreve um contêiner que contém recursos. No seu destruidor, a classe Container chama o método cleanup em dois dos seus membros Resource em paralelo e, em seguida, chama o método cleanup no 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 tarefa cria um Container objeto e a segunda tarefa cancela a tarefa geral. Para ilustração, o exemplo usa dois objetos concurrency::event para garantir que o cancelamento ocorra depois que o Container objeto é criado e que o Container objeto é destruído após a operação de cancelamento ocorrer.
// 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 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 comporte de forma diferente do esperado:
O cancelamento da tarefa pai faz com que a tarefa filho, a chamada para simultaneidade::p arallel_invoke, também seja cancelada. Por conseguinte, estes dois recursos não são libertados.
O cancelamento da tarefa pai faz com que a tarefa filho lance uma exceção interna. Como o
Containerdestruidor não manipula 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 através do
Containerdestruidor. Lançar a partir de um destrutor coloca a aplicação em um estado indefinido.
Recomendamos que você não execute operações críticas, como a liberação de recursos, em tarefas, a menos que possa 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 o destruidor de seus tipos.
[Topo]
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 tempo de execução crie muitos threads num curto período de tempo.
O Concurrency Runtime executa trabalho adicional quando uma tarefa termina, bloqueia cooperativamente ou cede. Quando uma iteração de loop paralelo é bloqueada, o tempo de execução do programa pode iniciar outra iteração. Quando não há threads ociosos disponíveis, o tempo de execução cria um novo thread.
Quando o corpo de um loop paralelo ocasionalmente bloqueia, esse mecanismo ajuda a maximizar o desempenho geral das tarefas. No entanto, quando muitas iterações bloqueiam, o tempo de execução pode criar muitos filamentos para realizar 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 parallel_for loop. Como send bloqueia cooperativamente, o tempo de execução do sistema cria um novo thread para executar trabalho adicional sempre 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ê refatore seu código para evitar esse padrão. Neste exemplo, você pode evitar a criação de threads adicionais chamando send num loop serial for.
[Topo]
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 realiza uma operação de bloqueio cooperativo, o sistema pode realizar outros trabalhos enquanto a primeira tarefa aguarda pela chegada dos dados. O ambiente de execução reagenda a tarefa em espera quando ela é desbloqueada. O tempo de execução normalmente reagenda tarefas que foram desbloqueadas mais recentemente antes de reagendar tarefas que foram desbloqueadas menos recentemente. Portanto, o tempo de execução pode agendar trabalho desnecessário durante a operação de bloqueio, o que leva à diminuição do desempenho. Assim, quando você executa 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 trabalho desnecessário.
Considere o exemplo a seguir que define a parallel_find_answer função, que procura um elemento da matriz fornecida que satisfaça a função de predicado fornecida. Quando a função de predicado retorna true, a função de trabalho paralelo cria um Answer 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. O runtime realiza outro trabalho somente quando a tarefa faz uma chamada de bloqueio cooperativa, como a 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 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;
}
[Topo]
Não escreva em dados partilhados num loop paralelo
O Concurrency Runtime fornece várias estruturas de dados, por exemplo, concurrency::critical_section, que sincronizam o acesso simultâneo aos dados compartilhados. Essas estruturas de dados são úteis em muitos casos, por exemplo, quando várias tarefas raramente exigem acesso compartilhado a um recurso.
Considere o exemplo seguinte que usa o algoritmo concurrency::parallel_for_each e um objeto critical_section para determinar a quantidade de números primos em um objeto std::array. Este exemplo não escala porque cada thread deve aguardar para aceder à 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();
});
Este exemplo também pode levar a um desempenho insatisfatório porque a operação de bloqueio freqüente serializa efetivamente o loop. Além disso, quando um objeto Concurrency Runtime executa uma operação de bloqueio, o agendador pode criar um thread adicional para executar outro trabalho enquanto o primeiro thread aguarda dados. Se o tempo de execução cria muitos threads porque muitas tarefas estão aguardando dados compartilhados, o aplicativo pode ter um desempenho insatisfatório ou entrar em um estado de poucos recursos.
O PPL define a classe concurrency::combinable , que ajuda a eliminar o estado compartilhado, fornecendo acesso a recursos compartilhados de forma sem bloqueio. A combinable classe fornece armazenamento local de thread que permite executar cálculos refinados e, em seguida, mesclar esses cálculos em um resultado final. Você pode pensar em um combinable objeto como uma variável de redução.
O exemplo a seguir modifica o anterior usando um combinable objeto em vez de um critical_section objeto para calcular a soma. Este exemplo é dimensionado porque cada thread contém sua própria cópia local da soma. Este exemplo 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, consulte Como usar combináveis para melhorar o desempenho. Para obter mais informações sobre a combinable classe, consulte Contêineres e objetos paralelos.
[Topo]
Sempre que possível, evite compartilhamentos falsos
O compartilhamento falso ocorre quando várias tarefas simultâneas em execução 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 as variáveis é invalidada. Cada processador deve recarregar a linha de cache sempre que a linha de cache for invalidada. Portanto, o compartilhamento falso pode causar diminuição do desempenho em seu aplicativo.
O exemplo básico a seguir mostra duas tarefas simultâneas que incrementam cada uma 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 para usar duas variáveis de contador. Este exemplo calcula o valor do contador final após a conclusão das tarefas. No entanto, este exemplo ilustra o compartilhamento falso porque as variáveis count1 e count2 provavelmente estarã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 é de 64 ou menos bytes.
Recomendamos que você use a classe concurrency::combinable quando precisar compartilhar dados entre tarefas. A combinable classe cria variáveis thread-local de tal forma que o compartilhamento falso é menos provável. Para obter mais informações sobre a combinable classe, consulte Contêineres e objetos paralelos.
[Topo]
Certifique-se de que as variáveis são válidas durante toda a vida útil de uma tarefa
Quando você fornece uma expressão lambda a um grupo de tarefas ou algoritmo paralelo, a cláusula capture especifica se o corpo da expressão lambda acessa variáveis no escopo de inclusão por valor ou por referência. Ao passar variáveis para uma expressão lambda por referência, você deve garantir que o tempo de vida dessa variável persista até que a tarefa seja concluída.
Considere o exemplo a seguir que define a object classe e a perform_action função. A perform_action função cria uma object variável e executa alguma ação sobre essa variável de forma assíncrona. Como não é garantido que a tarefa termine antes que a perform_action função retorne, o programa falhará ou exibirá um comportamento não especificado se a object variável 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 seu aplicativo, você pode usar uma das seguintes técnicas para garantir que as variáveis permaneçam válidas durante toda a vida útil de cada tarefa.
O exemplo a seguir passa a variável object por 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();
});
}
Como a object variável é passada por valor, quaisquer alterações de estado que ocorram nessa variável não aparecem na cópia original.
O exemplo a seguir usa o método concurrency::task_group::wait para garantir que a tarefa seja concluída antes que a perform_action função retorne.
// 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 que a função retorne, a perform_action função não se comporta mais de forma assíncrona.
O exemplo a seguir modifica a perform_action função para fazer uma referência à object variável. O chamador deve garantir que o tempo de vida da object variável 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();
});
}
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 algoritmo paralelo.
Para obter mais informações sobre expressões lambda, consulte Expressões lambda.
[Topo]
Ver também
Práticas recomendadas de tempo de execução de simultaneidade
Biblioteca de Padrões Paralelos (PPL)
Contêineres e objetos paralelos
Algoritmos paralelos
Cancelamento no PPL
Tratamento de exceções
Passo a passo: Criando uma rede Image-Processing
Como: Usar parallel_invoke para escrever uma rotina de classificação paralela
Como: Usar o cancelamento para interromper um loop paralelo
Como: Usar combináveis para melhorar o desempenho
Práticas recomendadas na biblioteca de agentes assíncronos
Boas práticas gerais no runtime de simultaneidade