| | | | |

2.2 Construtores básicos

Ajude a manter o site livre, gratuito e sem propagandas. Colabore!

Em revisão

2.2.1 Variáveis privadas e variáveis compartilhadas

Em revisão

Vamos analisar o seguinte código.

Código: vpc.cc
1#include <stdio.h>
2#include <omp.h>
3
4int main(int argc, char *argv[]) {
5
6  int tid, nt;
7
8  // regiao paralela
9  #pragma omp parallel
10  {
11    tid = omp_get_thread_num();
12    nt = omp_get_num_threads();
13
14    printf("Processo %d/%d\n", tid, nt);
15  }
16  printf("%d\n",nt);
17  return 0;
18}

Qual seria a saída esperada? Ao rodarmos este código, veremos uma saída da forma

Processo 0/4
Processo 2/4
Processo 3/4
Processo 3/4

Isto ocorre por uma situação de condição de corrida (race condition) entre os threads. As variáveis tid e nt foram declaradas antes da região paralela e, desta forma, são variáveis compartilhadas (shared variables) entre todos os threads na região paralela. Os locais na memória em que estas as variáveis estão alocadas é o mesmo para todos os threads.

A condição de corrida ocorre na linha 11. No caso da saída acima, as instâncias de processamento 1 e 3 entraram em uma condição de corrida no registro da variável tid.

Observação 2.2.1.

Devemos estar sempre atentos a uma possível condição de corrida. Este é um erro comum no desenvolvimento de códigos em paralelo.

Para evitarmos a condição de corrida, precisamos tornar a variável tid privada na região paralela. I.e., cada thread precisa ter uma variável tid privada. Podemos fazer isso alterando a linha 9 do código para

#pragma omp parallel private(tid)

Com essa alteração, a saída terá o formato esperado, como por exemplo

Processo 0/4
Processo 3/4
Processo 2/4
Processo 1/4

Faça a alteração e verifique!

Observação 2.2.2.

A diretiva #pragma omp parallel também aceita as instruções:

  • default(private|shared|none): o padrão é shared;

  • shared(var1, var2, ..., varn): para especificar explicitamente as variáveis que devem ser compartilhadas.

2.2.2 Laço e Redução

Em revisão

Vamos considerar o problema de computar

s=i=09999999911 (2.1)

em paralelo com np threads. Começamos analisando o seguinte código.

Código: soma0.cc
1#include <omp.h>
2#include <stdio.h>
3#include <math.h>
4
5int main(int argc, char *argv[]) {
6
7  int n = 999999991;
8
9  int s = 0;
10  #pragma omp parallel
11  {
12    int tid = omp_get_thread_num();
13    int nt = omp_get_num_threads();
14
15    int ini = (n+1)/nt*tid;
16    int fin = (n+1)/nt*(tid+1);
17    if (tid == nt-1)
18      fin = n+1;
19
20    for (int i=ini; i<fin; i++)
21      s += 1;
22  }
23
24  printf("%d\n",s);
25
26  return 0;
27}

Ao executarmos este código com nt>1, vamos ter saídas erradas. Verifique! Qual o valor esperado?

O erro do código está na condição de corrida (race condition) na linha 21. Esta é uma operação, ao ser iniciada por um thread, precisa ser terminada pelo thread antes que outro possa iniciá-la. Podemos fazer adicionando o construtor

#pragma omp critical

imediatamente antes da linha de código s += 1;. O código fica como segue, verifique!

Código: soma1.cc
1#include <omp.h>
2#include <stdio.h>
3#include <math.h>
4
5int main(int argc, char *argv[]) {
6
7  int n = 999999991;
8
9  int s = 0;
10  #pragma omp parallel
11  {
12    int tid = omp_get_thread_num();
13    int nt = omp_get_num_threads();
14
15    int ini = (n+1)/nt*tid;
16    int fin = (n+1)/nt*(tid+1);
17    if (tid == nt-1)
18      fin = n+1;
19
20    for (int i=ini; i<fin; i++)
21      #pragma omp critical
22      s += 1;
23  }
24
25  printf("%d\n",s);
26
27  return 0;
28}

Esta abordagem evita a condição de corrida e fornece a resposta esperada. No entanto, ela acaba serializando o código, o qual é será muito mais lento que o código serial. Verifique!

Observação 2.2.3.

A utilização do construtor

#pragma omp critical

reduz a performance do código e só deve ser usada quando realmente necessária.

Uma alternativa é alocar as somas parciais de cada thread em uma variável privada e, ao final, somar as partes computadas. Isto pode ser feito com o seguinte código. Verifique!

Código: soma2.cc
1#include <omp.h>
2#include <stdio.h>
3#include <math.h>
4
5int main(int argc, char *argv[]) {
6
7  int n = 999999991;
8
9  int s = 0;
10  #pragma omp parallel
11  {
12    int tid = omp_get_thread_num();
13    int nt = omp_get_num_threads();
14
15    int ini = (n+1)/nt*tid;
16    int fin = (n+1)/nt*(tid+1);
17    if (tid == nt-1)
18      fin = n+1;
19
20    int st = 0;
21    for (int i=ini; i<fin; i++)
22      st += 1;
23
24    #pragma omp critical
25    s += st;
26  }
27
28  printf("%d\n",s);
29
30  return 0;
31}

Este último código pode ser simplificado usando o construtor

#pragma omp for

Com este construtor, o laço do somatório pode ser automaticamente distribuindo entre os threads. Verifique o seguinte código!

Código: somafor.cc
1#include <omp.h>
2#include <stdio.h>
3#include <math.h>
4
5int main(int argc, char *argv[]) {
6
7  int n = 999999991;
8
9  int s = 0;
10  #pragma omp parallel
11  {
12    int st = 0;
13
14    #pragma omp for
15    for (int i=0; i<n; i++)
16      st += 1;
17
18    #pragma omp critical
19    s += st;
20  }
21  printf('%d\n',s);
22  return 0;
23}

Mais simples e otimizado, é automatizar a operação de redução (no caso, a soma das somas parciais) adicionado

reduction(+: s)

ao construtor que inicializa a região paralela. Verifique o seguinte código!

Código: soma.cc
1#include <stdio.h>
2#include <math.h>
3
4#include <omp.h>
5
6int main(int argc, char *argv[]) {
7
8  int n = 999999991;
9
10  int s = 0;
11
12  #pragma omp parallel for reduction(+: s)
13  for (int i=0; i<n+1; i++)
14    s += 1;
15
16  printf("%d\n",s);
17  return 0;
18}
Observação 2.2.4.

A instrução de redução pode ser usada com qualquer operação binária aritmética (+, -, /, *), lógica (&, |) ou procedimentos intrínsecos (max, min).

2.2.3 Sincronização

Em revisão

A sincronização dos threads deve ser evitada sempre que possível, devido a perda de performance em códigos paralelos. Atenção, ela ocorre implicitamente no término da região paralela!

Barreira

Em revisão

No seguinte código, o thread 1 é atrasado em 1 segundo, de forma que ele é o último a imprimir. Verifique!

Código: sinc0.cc
1#include <stdio.h>
2#include <ctime>
3#include <omp.h>
4
5int main(int argc, char *argv[]) {
6
7  // master thread id
8  int tid = 0;
9  int nt;
10
11  #pragma omp parallel private(tid)
12  {
13    tid = omp_get_thread_num();
14    nt = omp_get_num_threads();
15
16    if (tid == 1) {
17      // delay 1s
18      time_t t0 = time(NULL);
19      while (time(NULL) - t0 < 1) {
20      }
21    }
22
23    printf("Processo %d/%d.\n", tid, nt);
24  }
25  return 0;
26}

Agora, podemos forçar a sincronização dos threads usando o construtor

#pragma omp barrier

em uma determinada linha do código. Por exemplo, podemos fazer todos os threads esperarem pelo thread 1 no código acima. Veja a seguir o código modificado. Teste!

Código: sinc1.cc
1#include <stdio.h>
2#include <ctime>
3#include <omp.h>
4
5int main(int argc, char *argv[]) {
6
7  // master thread id
8  int tid = 0;
9  int nt;
10
11  #pragma omp parallel private(tid)
12  {
13    tid = omp_get_thread_num();
14    nt = omp_get_num_threads();
15
16    if (tid == 1) {
17      // delay 1s
18      time_t t0 = time(NULL);
19      while (time(NULL) - t0 < 1) {
20      }
21    }
22
23    #pragma omp barrier
24
25    printf("Processo %d/%d.\n", tid, nt);
26  }
27  return 0;
28}

Seção

Em revisão

O construtor sections pode ser usado para determinar seções do código que deve ser executada de forma serial apenas uma vez por um único thread. Verifique o seguinte código.

Código: secao.cc
1#include <stdio.h>
2#include <ctime>
3#include <omp.h>
4
5int main(int argc, char *argv[]) {
6
7  // master thread id
8  int tid = 0;
9  int nt;
10
11  // regiao paralela
12  #pragma omp parallel private(tid)
13  {
14    tid = omp_get_thread_num();
15    nt = omp_get_num_threads();
16
17    #pragma omp sections
18    {
19      // secao 1
20      #pragma omp section
21      {
22        printf("%d/%d exec secao 1\n", \
23               tid, nt);
24      }
25
26      // secao 2
27      #pragma omp section
28      {
29        // delay 1s
30        time_t t0 = time(NULL);
31        while (time(NULL) - t0 < 1) {
32        }
33        printf("%d/%d exec a secao 2\n", \
34               tid, nt);
35      }
36    }
37
38    printf("%d/%d terminou\n", tid, nt);
39  }
40
41  return 0;
42}

No código acima, o primeiro thread que alcançar a linha 19 é o único a executar a seção 1 e, o primeiro que alcançar a linha 25 é o único a executar a seção 2.

Observe que ocorre a sincronização implícita de todos os threads ao final do escopo sections. Isso pode ser evitado usando a cláusula nowait, i.e. alterando a linha 16 para

# pragma omp sections nowait

Teste!

Observação 2.2.5.

A clausula nowait também pode ser usada com o construtor for, i.e.

#pragma omp for nowait

Para uma região contendo apenas uma seção, pode-se usar o construtor

#pragma omp single

Isto é equivalente a escrever

#pragma omp sections
  #pragma omp section

2.2.4 Exercícios Resolvidos

Em revisão

ER 2.2.1.

Escreva um código MP para computar o produto escalar entre dois vetores de n pontos flutuantes randômicos.

Solução.

A solução é dada no código a seguir.

Código: prodesc.cc
1// io
2#include <stdio.h>
3// rand
4#include <cstdlib>
5// time
6#include <ctime>
7// openMP
8#include <omp.h>
9
10#define n 99999
11
12int main(int argc, char *argv[]) {
13
14  double a[n], b[n];
15
16  // inicializa rand
17  srand(time(NULL));
18
19  // inicializa os vetores
20  #pragma omp parallel for
21  for (int i=0; i<n; i++) {
22    a[i] = double(rand())/RAND_MAX;
23    b[i] = double(rand())/RAND_MAX;
24  }
25
26  // produto escalar
27  double dot = 0;
28  #pragma omp parallel for reduction(+: dot)
29  for (int i=0; i<n; i++)
30    dot += a[i] * b[i];
31
32  printf("%lf\n",dot);
33
34  return 0;
35}
ER 2.2.2.

Faça um código MP para computar a multiplicação de uma matriz A n×n por um vetor de n elementos (pontos flutuantes randômicos). Utilize o construtor omp sections para distribuir a computação entre somente dois threads.

Solução.

A solução é dada no código a seguir.

Código: AxSecoes.cc
1#include <stdio.h>
2#include <cstdlib>
3#include <ctime>
4#include <omp.h>
5
6#define n 9999
7
8int main(int argc, char *argv[]) {
9
10  // matriz
11  double a[n][n];
12  // vetores
13  double x[n], y[n];
14
15  // inicializa rand
16  srand(time(NULL));
17
18  // inicializacao
19  for (int i=0; i<n; i++) {
20    for (int j=0; j<n; j++) {
21      a[i][j] = double(rand())/RAND_MAX;
22    }
23    x[i] = double(rand())/RAND_MAX;
24
25    y[i] = 0.;
26  }
27
28  // y = A*x
29  #pragma omp parallel sections
30  {
31    #pragma omp section
32    {
33      for (int i=0; i<n/2; i++)
34        for (int j=0; j<n; j++)
35          y[i] += a[i][j] * x[j];
36    }
37
38    #pragma omp section
39    {
40      for (int i=n/2; i<n; i++)
41        for (int j=0; j<n; j++)
42          y[i] += a[i][j] * x[j];
43    }
44  }
45
46  return 0;
47}

2.2.5 Exercícios

Em revisão

E. 2.2.1.

Considere o seguinte código

1    int tid = 10;
2    #pragma omp parallel private(tid)
3    {
4      tid = omp_get_thread_num();
5    }
6    printf("%d\n", tid);

Qual o valor impresso?

E. 2.2.2.

Escreva um código MP para computar uma aproximação para

I=11ex2𝑑x (2.2)

usando a regra composta do trapézio com n subintervalos uniformes.

E. 2.2.3.

Escreva um código MP para computar uma aproximação para

I=11ex2𝑑x (2.3)

usando a regra composta de Simpson com n subintervalos uniformes. Dica: evite sincronizações desnecessárias!

E. 2.2.4.

Escreva um código MP para computar a multiplicação de uma matriz A n×n por um vetor x de n elementos (pontos flutuantes randômicos). Faça o código de forma a suportar uma arquitetura com np1 threads.

E. 2.2.5.

Escreva um código MP para computar o produto de duas matrizes n×n de pontos flutuantes randômicos. Utilize o construtor omp sections para distribuir a computação entre somente dois threads.

E. 2.2.6.

Escreva um código MP para computar o produto de duas matrizes n×n de pontos flutuantes randômicos. Faça o código de forma a suportar uma arquitetura com np1 threads.


Envie seu comentário

As informações preenchidas são enviadas por e-mail para o desenvolvedor do site e tratadas de forma privada. Consulte a Política de Uso de Dados para mais informações. Aproveito para agradecer a todas/os que de forma assídua ou esporádica contribuem enviando correções, sugestões e críticas!