9 março, 2013 2 Comentários AUTOR: elemarjr CATEGORIAS: Sem categoria Tags:, , ,

Fundamentos de PPL (C++) - Parte 1 - Tasks

Tempo de leitura: Menos de um minuto

Olá. Tudo certo?!

PPL (Parallel Patterns Library) é uma biblioteca C++ que objetiva facilitar o desenvolvimento de aplicações com suporte a paralelismo.

Podemos entender essa biblioteca através das seguintes iniciativas:

  1. Paralelismo de tarefas (Tasks) - que permite a execução de duas ou mais atividades simultaneamente;
  2. Algoritmos e "containers" - métodos genéricos de processamento combinados com estruturas de dados com suporte pronto para acesso e modificação em paralelo.

Em muitos aspectos, PPL lembra a TPL (Task Parallel Library) que está disponível para desenvolvedores .NET. Além disso, "algoritmos e containers" parece bem influenciado pelos padrões seguidos na STL.

Um exemplo muito simples

Para começar a entender o poder dessa biblioteca, comecemos por um exemplo extremamente simples.

#include
#include

using namespace std;
using namespace concurrency;

int fibonacci(int number)
{
if (number < 2) return number; return fibonacci(number - 1) + fibonacci(number - 2); } int main() { int fib35, fib40; parallel_invoke( [&] { fib35 = fibonacci(35); }, [&] { fib40 = fibonacci(30); } ); cout << "Using parallel_invoke: " << endl << "fib(35) = " << fib35 << endl << "fib(40) = " << fib40 << endl << endl; } [/code] Pegou a ideia?! A função parallel_invoke é a "porta de entrada" para a PPL. Esta função cria uma "task group" com  uma "execução paralela" para cada expressão lambda fornecida em sua lista de argumentos.

Reproduzindo o comportamento de parallel_invoke

Podemos obter o mesmo resultado de parallel_invoke usando o objeto task_group. Veja só:

int main()
{
int fib35, fib40;

task_group tg;

tg.run([&] { fib35 = fibonacci(35); });
tg.run([&] { fib40 = fibonacci(40); });

tg.wait();

cout << "Using task_group: " << endl << "fib(35) = " << fib35 << endl << "fib(40) = " << fib40 << endl << endl; return 0; } [/code] Certo? O método run cria e agenda uma nova task. O argumento desse método deve ser uma expressão lambda, ou um ponteiro para uma função, ou um "objeto" função que será executado quando a task entrar em atividade.
O método wait interrompe a execução do fluxo atual até que todas as tarefas tenham sido executadas.

Colocando tudo junto

Para concluirmos, vejamos um comparativo entre as abordagens e a execução não paralela.

#include
#include
#include

using namespace std;
using namespace concurrency;

template
__int64 time_call(TFunction&& function)
{
__int64 begin = GetTickCount();
function();
return GetTickCount() - begin;
}

int fibonacci(int number)
{
if (number < 2) return number; return fibonacci(number - 1) + fibonacci(number - 2); } void NoParallel() { int fib35, fib40; fib35 = fibonacci(35); fib40 = fibonacci(40); cout << "No Parallel: " << endl << "fib(35) = " << fib35 << endl << "fib(40) = " << fib40 << endl << endl; } void UsingParallelInvoke() { int fib35, fib40; parallel_invoke( [&] { fib35 = fibonacci(35); }, [&] { fib40 = fibonacci(40); } ); cout << "Using parallel_invoke: " << endl << "fib(35) = " << fib35 << endl << "fib(40) = " << fib40 << endl << endl; } void UsingTaskGroup() { int fib35, fib40; task_group tg; tg.run([&] { fib35 = fibonacci(35); }); tg.run_and_wait([&] { fib40 = fibonacci(40); }); cout << "Using task_group: " << endl << "fib(35) = " << fib35 << endl << "fib(40) = " << fib40 << endl << endl; } int main() { __int64 noParallelTime = time_call(NoParallel); __int64 parallelInvokeTime = time_call(UsingParallelInvoke); __int64 taskGroupTime = time_call(UsingTaskGroup); cout << "No Parallel : " << noParallelTime << "ms. " << endl << "Parallel Invoke: " << parallelInvokeTime << "ms. " << endl << "Task Group : " << taskGroupTime << "ms. " << endl ; return 0; } [/code] Se executar, perceberá que a diferença de desempenho é muito pequena. Por quê? Simples! Estamos usando abordagens pouco eficientes. Voltaremos.