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.