OpenMP é uma API que simplifica o paralelismo em C, C++ e Fortran. Ela utiliza diretivas (pragmas) no código-fonte para criar e gerenciar threads de forma prática, sem a necessidade de manipular threads manualmente.
Compilação:
gcc -fopenmp exemplo.c -o exemplo
O parâmetro -fopenmp
habilita o suporte ao OpenMP no compilador GCC.
Alterando a quantidade de threads:
export OMP_NUM_THREADS=3
O comando export OMP_NUM_THREADS
altera a quantidade de threads.
Execução com contagem de tempo:
time ./exemplo
O comando time
mede o tempo total de execução do programa no sistema.
Exemplo básico:
#include stdio.h #include omp.h int main() { omp_set_num_threads(4); // Define o número de threads a serem usadas double inicio = omp_get_wtime(); #pragma omp parallel { int id = omp_get_thread_num(); printf("Olá da thread %d\n", id); } double fim = omp_get_wtime(); printf("Tempo interno: %f segundos\n", fim - inicio); return 0; }
Esse programa demonstra os conceitos básicos do OpenMP:
omp_set_num_threads(4)
: Define explicitamente que o programa usará 4 threads. Se omitido, o OpenMP usa o número de núcleos disponíveis no processador por padrão.#pragma omp parallel
: Inicia um bloco paralelo, criando um time de threads (neste caso, 4). Cada thread executa o código dentro das chaves {}
.omp_get_thread_num()
: Retorna o ID da thread atual (de 0 a 3, com 4 threads).omp_get_wtime()
: Mede o tempo decorrido internamente, útil para avaliar o desempenho do paralelismo.A saída mostra "Olá da thread X" para cada thread ativa e o tempo total de execução do bloco paralelo.
Exemplo adicional: Medição de desempenho
#include stdio.h #include omp.h #include math.h int main() { int n = 10000000; double sum = 0.0; // Serial double start_serial = omp_get_wtime(); for (int i = 0; i < n; i++) { sum += sin(i) * cos(i); } double end_serial = omp_get_wtime(); // Parallel double sum_parallel = 0.0; double start_parallel = omp_get_wtime(); #pragma omp parallel for reduction(+:sum_parallel) for (int i = 0; i < n; i++) { sum_parallel += sin(i) * cos(i); } double end_parallel = omp_get_wtime(); printf("Serial: %f sec, Result: %f\n", end_serial - start_serial, sum); printf("Parallel: %f sec, Result: %f\n", end_parallel - start_parallel, sum_parallel); printf("Speedup: %.2fx\n", (end_serial-start_serial)/(end_parallel-start_parallel)); return 0; }
Este exemplo mostra como medir o ganho de desempenho (speedup) ao paralelizar um cálculo matemático intensivo.
A diretiva #pragma omp parallel for
é uma das mais poderosas do OpenMP. Ela combina a criação de um bloco paralelo (#pragma omp parallel
) com a distribuição automática das iterações de um loop entre as threads disponíveis. Isso reduz a complexidade de gerenciar paralelismo em loops.
Como funciona:
omp_set_num_threads()
ou pela variável de ambiente OMP_NUM_THREADS
.Exemplo básico:
#include stdio.h #include omp.h int main() { omp_set_num_threads(4); // Define 4 threads para o exemplo int i; #pragma omp parallel for for (i = 0; i < 10; i++) { printf("Thread %d executando iteração %d\n", omp_get_thread_num(), i); } return 0; }
Neste exemplo, as 10 iterações (0 a 9) são divididas entre 4 threads. Por exemplo, a thread 0 pode executar as iterações 0-2, a thread 1 as iterações 3-5, e assim por diante, dependendo da estratégia de escalonamento do OpenMP.
Exemplo prático com cálculo:
#include#include int main() { int n = 1000000; // Um milhão de elementos double soma = 0.0; omp_set_num_threads(4); double inicio = omp_get_wtime(); #pragma omp parallel for reduction(+:soma) for (int i = 0; i < n; i++) { soma += 1.0 / (i + 1); // Soma harmônica parcial } double fim = omp_get_wtime(); printf("Soma: %.10f\n", soma); printf("Tempo: %f segundos\n", fim - inicio); return 0; }
Este exemplo calcula uma soma harmônica parcial em paralelo:
reduction(+:soma)
: Garante que a variável soma
seja atualizada corretamente entre as threads, somando os resultados parciais de cada uma ao final.Variações úteis:
#pragma omp parallel for schedule(static)
: Divide as iterações em blocos iguais (padrão).#pragma omp parallel for schedule(dynamic, 1)
: Distribui iterações dinamicamente, útil para cargas de trabalho desbalanceadas.O schedule
controla como as iterações são alocadas, permitindo otimização conforme a natureza do problema.
Exemplo adicional: Escalonamento dinâmico
#include stdio.h #include omp.h #include unistd.h void heavy_work(int i) { usleep(i * 1000); // Simula trabalho variável } int main() { int n = 20; printf("Static schedule:\n"); #pragma omp parallel for schedule(static) for (int i = 0; i < n; i++) { printf("Thread %d processing %d\n", omp_get_thread_num(), i); heavy_work(i); } printf("\nDynamic schedule:\n"); #pragma omp parallel for schedule(dynamic, 1) for (int i = 0; i < n; i++) { printf("Thread %d processing %d\n", omp_get_thread_num(), i); heavy_work(i); } return 0; }
Este exemplo demonstra a diferença entre escalonamento estático e dinâmico, útil quando as iterações têm cargas de trabalho desbalanceadas.
A diretiva #pragma omp sections
divide blocos de código entre threads, enquanto #pragma omp task
cria tarefas independentes.
Exemplo com sections:
#pragma omp parallel sections { #pragma omp section printf("Seção 1 por thread %d\n", omp_get_thread_num()); #pragma omp section printf("Seção 2 por thread %d\n", omp_get_thread_num()); }
Exemplo avançado com tasks
#include stdio.h #include omp.h #include stdlib.h int fib(int n) { if (n < 2) return n; int x, y; #pragma omp task shared(x) x = fib(n-1); #pragma omp task shared(y) y = fib(n-2); #pragma omp taskwait return x + y; } int main() { int n = 10; double start = omp_get_wtime(); #pragma omp parallel { #pragma omp single { printf("Fib(%d) = %d\n", n, fib(n)); } } double end = omp_get_wtime(); printf("Time: %f seconds\n", end - start); return 0; }
Este exemplo implementa recursivamente a sequência de Fibonacci usando tasks, demonstrando como paralelizar algoritmos recursivos.
No OpenMP, o controle de variáveis é essencial para garantir o funcionamento correto do paralelismo. As variáveis podem ser classificadas como:
shared
: Compartilhadas entre todas as threads. Todas acessam a mesma instância da variável.private
: Cada thread tem sua própria cópia independente da variável, inicializada como não definida (exceto em alguns casos específicos).Comportamento padrão:
Por padrão, variáveis declaradas fora de um bloco paralelo são shared
, enquanto variáveis locais dentro do bloco (como o índice de um loop) precisam ser explicitamente declaradas como private
para evitar conflitos.
Exemplo básico:
#include stdio.h #include omp.h int main() { int i; omp_set_num_threads(4); #pragma omp parallel for private(i) for (i = 0; i < 5; i++) { printf("Thread %d, valor de i: %d\n", omp_get_thread_num(), i); } return 0; }
Neste exemplo, i
é declarado como private
. Cada thread tem sua própria cópia de i
, que é usada para controlar as iterações que ela executa. Sem o private(i)
, haveria uma condição de corrida, pois todas as threads tentariam modificar a mesma variável i
.
Exemplo com shared e private:
#include stdio.h #include omp.h int main() { int contador = 0; // Variável compartilhada omp_set_num_threads(4); #pragma omp parallel private(contador_privado) { int contador_privado = omp_get_thread_num(); // Variável privada contador_privado += 1; // Cada thread modifica sua cópia #pragma omp critical contador += 1; // Acesso seguro à variável compartilhada printf("Thread %d: contador_privado = %d, contador compartilhado = %d\n", omp_get_thread_num(), contador_privado, contador); } return 0; }
Explicação:
contador
: É shared
por padrão, pois foi declarada fora do bloco paralelo. Usamos #pragma omp critical
para evitar condições de corrida ao modificá-la.contador_privado
: Declarada como private
, cada thread inicializa e modifica sua própria cópia sem interferir nas outras.contador_privado
(baseado no ID da thread) e o contador
compartilhado é incrementado de forma segura.Exemplo prático com array:
#include stdio.h #include omp.h int main() { int n = 10; int array[10]; omp_set_num_threads(4); #pragma omp parallel for private(i) for (int i = 0; i < n; i++) { array[i] = i * omp_get_thread_num(); // Cada thread escreve no array printf("Thread %d escreveu array[%d] = %d\n", omp_get_thread_num(), i, array[i]); } printf("Resultado final do array: "); for (int i = 0; i < n; i++) { printf("%d ", array[i]); } printf("\n"); return 0; }
Neste caso:
array
é shared
: Todas as threads escrevem em diferentes posições dele.i
é private
: Cada thread usa seu próprio i
para acessar uma parte do loop.critical
aqui, pois cada thread modifica uma posição distinta do array (sem sobreposição).Cuidados:
shared
podem causar condições de corrida se não forem protegidas (ex.: com critical
ou atomic
).private
não mantêm seu valor após o bloco paralelo, pois são locais às threads.Exemplo avançado: firstprivate e lastprivate
#include stdio.h #include omp.h int main() { int x = 10, y = 20, z = 30; printf("Before parallel: x=%d, y=%d, z=%d\n", x, y, z); #pragma omp parallel for firstprivate(x) lastprivate(y) private(z) for (int i = 0; i < 4; i++) { printf("Thread %d: x=%d, y=%d, z=%d\n", omp_get_thread_num(), x, y, z); x += i; // Modifica cópia local y = i; // Será sobrescrito pela última iteração z = 100; // Modifica cópia local } printf("After parallel: x=%d, y=%d, z=%d\n", x, y, z); return 0; }
Este exemplo demonstra:
firstprivate
: Inicializa a variável privada com o valor da variável originallastprivate
: Atualiza a variável original com o valor da última iteraçãoprivate
: Cria uma cópia não inicializada para cada threadNo OpenMP, quando múltiplas threads acessam ou modificam uma variável compartilhada simultaneamente, podem ocorrer condições de corrida, resultando em resultados inconsistentes. Para evitar isso, usamos:
#pragma omp critical
: Protege uma seção crítica do código, garantindo que apenas uma thread por vez a execute.#pragma omp atomic
: Garante que uma operação simples (como incremento ou atribuição) seja realizada de forma atômica, ou seja, sem interrupções por outras threads.Diferenças principais:
critical
: Mais genérico, protege qualquer bloco de código, mas é mais lento devido à sincronização ampla.atomic
: Mais eficiente, mas limitado a operações básicas (ex.: +=
, -=
, *=
, etc.).Exemplo básico com critical:
#include#include int main() { int soma = 0; omp_set_num_threads(4); #pragma omp parallel for for (int i = 0; i < 100; i++) { #pragma omp critical soma += i; // Apenas uma thread por vez atualiza soma } printf("Soma: %d (esperado: 4950)\n", soma); return 0; }
Neste exemplo, sem o critical
, as threads poderiam sobrescrever as atualizações umas das outras em soma
. Com critical
, a soma de 0 a 99 (4950) é calculada corretamente, mas a execução é mais lenta devido à exclusão mútua.
Exemplo com atomic:
#include stdio.h #include omp.h int main() { int soma = 0; omp_set_num_threads(4); double inicio = omp_get_wtime(); #pragma omp parallel for for (int i = 0; i < 100; i++) { #pragma omp atomic soma += i; // Operação atômica, mais eficiente } double fim = omp_get_wtime(); printf("Soma: %d (esperado: 4950)\n", soma); printf("Tempo: %f segundos\n", fim - inicio); return 0; }
Aqui, atomic
substitui critical
. Como a operação é um simples incremento, atomic
é mais rápido, pois evita a sincronização completa de um bloco crítico.
Exemplo prático com critical e múltiplas operações:
#include stdio.h #include omp.h int main() { int soma = 0, maior = 0; omp_set_num_threads(4); #pragma omp parallel for for (int i = 0; i < 100; i++) { #pragma omp critical { soma += i; // Soma acumulada if (i > maior) // Atualiza maior valor maior = i; } } printf("Soma: %d, Maior: %d\n", soma, maior); return 0; }
Neste caso, usamos critical
porque temos múltiplas operações (soma e comparação) que dependem de variáveis compartilhadas. O atomic
não seria suficiente aqui, pois só protege uma única operação.
Boas práticas:
atomic
para operações simples (ex.: incrementos, atribuições) devido ao melhor desempenho.critical
para blocos complexos ou quando múltiplas operações precisam ser sincronizadas.#pragma omp critical(nome)
para diferenciar seções críticas distintas.Exemplo com critical nomeado:
#pragma omp parallel { #pragma omp critical(soma_crit) soma += 1; #pragma omp critical(print_crit) printf("Thread %d\n", omp_get_thread_num()); }
Seções críticas nomeadas permitem que diferentes blocos sejam sincronizados independentemente.
Exemplo comparativo: critical vs atomic vs reduction
#include stdio.h #include omp.h #include time.h #define N 1000000 int main() { int sum_critical = 0, sum_atomic = 0, sum_reduction = 0; // Método 1: Critical double start = omp_get_wtime(); #pragma omp parallel for for (int i = 0; i < N; i++) { #pragma omp critical sum_critical += i; } double end_critical = omp_get_wtime(); // Método 2: Atomic #pragma omp parallel for for (int i = 0; i < N; i++) { #pragma omp atomic sum_atomic += i; } double end_atomic = omp_get_wtime(); // Método 3: Reduction #pragma omp parallel for reduction(+:sum_reduction) for (int i = 0; i < N; i++) { sum_reduction += i; } double end_reduction = omp_get_wtime(); printf("Critical: %f sec\n", end_critical - start); printf("Atomic: %f sec\n", end_atomic - end_critical); printf("Reduction: %f sec\n", end_reduction - end_atomic); return 0; }
Este exemplo compara três métodos para realizar uma soma em paralelo, mostrando que reduction
é geralmente o mais eficiente.
1. Qual diretiva inicia um bloco paralelo no OpenMP?
2. O que a diretiva #pragma omp parallel for
faz?
3. Qual é a diferença entre #pragma omp sections
e #pragma omp task
?
4. O que acontece se uma variável não for declarada como private
em um loop paralelo?
5. Quando devemos preferir #pragma omp atomic
em vez de #pragma omp critical
?
1. Qual a diferença entre #pragma omp parallel for
e #pragma omp for
?
#pragma omp parallel for
combina a criação de um bloco paralelo com a divisão das iterações de um loop entre threads, enquanto #pragma omp for
apenas divide as iterações entre threads já existentes em um bloco paralelo iniciado por #pragma omp parallel
. Em resumo, parallel for
é um atalho para paralelismo e divisão, enquanto for
exige um contexto paralelo prévio.2. Como o OpenMP decide quantas threads usar se omp_set_num_threads
não for chamado?
omp_set_num_threads
não for especificado, o OpenMP usa o número de núcleos disponíveis no processador como padrão. Alternativamente, o número de threads pode ser definido pela variável de ambiente OMP_NUM_THREADS
(ex.: export OMP_NUM_THREADS=4
), que tem prioridade sobre o padrão do sistema se configurada.3. Qual é a vantagem de usar #pragma omp sections
em vez de executar blocos de código sequencialmente?
#pragma omp sections
permite que blocos de código independentes sejam executados em paralelo por diferentes threads, reduzindo o tempo total de execução em comparação com uma execução sequencial. Cada #pragma omp section
dentro de sections
é atribuído a uma thread, aproveitando o paralelismo para tarefas distintas que não dependem umas das outras.4. Por que declarar uma variável como private
é importante em um loop paralelo?
private
em um loop paralelo (ex.: #pragma omp parallel for private(i)
) é crucial para evitar condições de corrida. Sem isso, a variável seria shared
por padrão, permitindo que todas as threads a modificassem simultaneamente, o que poderia levar a resultados imprevisíveis. Com private
, cada thread tem sua própria cópia, garantindo independência e consistência.5. Em que situações #pragma omp critical
é preferível a #pragma omp atomic
?
#pragma omp critical
é preferível quando múltiplas operações ou um bloco de código complexo precisam ser protegidos contra acesso concorrente (ex.: atualizar duas variáveis compartilhadas ou executar uma lógica condicional). Já o #pragma omp atomic
é limitado a operações simples, como incrementos ou atribuições, sendo mais eficiente nesses casos. Use critical
para flexibilidade e atomic
para desempenho em tarefas básicas.