OpenMP é uma extensão para as linguagens C, C++ e Fortran que permite a paralelização de programas utilizando o modelo de Memória compartilhada através da inserção de diretivas (pragmas) em um código sequencial. Utilizando os recursos da API OpenMP, é possível reduzir significativamente as dificuldades de implementação introduzidas por APIs de mais baixo nível, como a biblioteca pthreads.
As diretivas introduzidas no código são interpretadas pelo compilador (que deve fornecer suporte à OpenMP), e então são utilizadas para gerar código paralelo que utiliza APIs de mais baixo nível (geralmente a biblioteca pthreads). É possível controlar o processo de compilação através de parâmetros disponíveis através de variáveis de ambiente.
Regiões paralelas
Em OpenMP, uma região paralela é um bloco de código que será executado por um time de threads. Uma região paralela pode ser definida através da diretiva #pragma omp parallel
, que cria um time de threads para executar o bloco logo abaixo da diretiva.
Definindo o número de threads
O número padrão de threads é igual ao número de unidades de processamento disponíveis do sistema, mas é possível alterar esse número de múltiplas formas.
A diretiva if
permite que a execução paralela seja condicionada ao resultado de uma expressão. Se o resultado da expressão for verdadeiro, a região é executada em paralelo, caso contrário, é executada sequencialmente (em uma única thread).
A diretiva num_threads
permite a definição do número de threads que irão executar um determinado bloco.
A função omp_set_num_threads
define o número de threads que serão utilizadas para executar todas as regiões paralelas subsequentes que não especificarem explicitamente a clausula num_threads
.
Também é possível definir o número de threads utilizadas pelo OpenMP através da variável de ambiente OMP_NUM_THREADS
. Quando definido, o valor dessa variável passa a ser o novo número padrão de threads.
Divisão de trabalho
As diretivas da OpenMP oferecem variados métodos de divisão de trabalho, sendo possível configurar os detalhes de como a carga de trabalho será dividida entre as threads do time.
Single
A diretiva single
é utilizada para especificar que uma determinada região será executada por apenas uma thread. Uma barreira é implicitamente colocada ao final do bloco da diretiva, fazendo com que as demais threads do time aguardem o término da execução do bloco, a não ser que uma claúsula nowait
seja especificada.
Sections
A diretiva sections
define seções de código que serão executadas apenas uma vez por uma das threads do time. Esse modelo de divisão de trabalho pode ser utilizado para implementar a decomposição funcional do programa. Ao final das sections uma barreira é inserida, a não ser que uma clausula nowait
seja especificada.
For
A diretiva for
permite dividir as iterações de um loop entre várias threads para executá-las em paralelo. Em geral, é essa diretiva permite implementar a decomposição de domínio. Por padrão as iterações do loop são divididas igualmente entre as threads do time, de forma que para um loop de n
iterações executado por t
threads, cada thread executa n/t
iterações. Vale destacar que por padrão a variável de controle do loop é implicitamente privada, ou seja, cada thread tem sua cópia da variável. Isso permite que uma thread não incremente ou decremente o número da iteração de outra, potencialmente produzindo resultados incorretos.
O método de divisão das iterações de um for loop é definido através de políticas que podem declaradas diferentes maneiras: com uso da cláusula schedule
na diretiva for
, com a função omp_set_schedule
ou mesmo com a variável de ambiente OMP_SCHEDULE
.
As políticas disponíveis são as seguintes:
static
: as iterações são divididas de maneira estática em porções de um tamanho definido, se o tamanho não for especificado então é feita uma divisão uniforme.dynamic
: as iterações são divididas em porções de um tamanho definido (por padrão o tamanho é 1) e então são atribuídas dinamicamente às threads, ao finalizar uma porção das iterações, a thread inicia o processamento de outra porção.guided
: o número de iterações atribuídas à cada thread é calculado em função do número de iterações restantes.runtime
: a política de divisão é determinada apenas em runtime através da variável de ambienteOMP_SCHEDULE
.auto
: a política de divisão é determinada de forma automática pelo compilador.
É possível ainda agregar os resultados das iterações executadas por diferentes threads em uma única variável de acordo com um operador lógico ou aritmético através da cláusula reduction
. Reduções são especialmente úteis para agregar resultados parciais obtidos por loops paralelos.
Criação de tarefas sob demanda
Em OpenMP, tasks são tarefas que podem ser criadas dinamicamente para serem executadas por alguma thread do time de threads da região paralela atual. Tasks podem ser criadas através da diretiva task
, e é possível aguardar a execução das tasks com a diretiva taskwait
.
Controlando o acesso à variáveis
Em OpenMP é possível controlar o acesso e visibilidade de variáveis através da definição de variáveis privadas e de mecanismos de exclusão mútua.
Por padrão as variáveis acessíveis no escopo da região paralela são compartilhadas entre todas a threads do time, a não ser que seja especificada uma diretiva private
com as variáveis privadas ou nos casos de diretivas for
que automaticamente tornam a variável de controle da iteração privada. Quando especificada, a claúsula private
torna as variáveis passadas como argumento privadas para cada thread, ou seja, as variáveis se tornam locais na rotina de cada thread.
A exclusão mútua de variáveis compartilhadas pode ser obtida através da diretiva critical
. Essa diretiva define que um bloco da região paralela deve ser executado em exclusão mútua, ou seja, apenas uma thread do time pode executar o bloco em um determinado momento do tempo.
A diretiva atomic
especifica que uma determinada instrução deve ser realizada de forma atômica. Quando possível, essa diretiva faz com que a operação seja executada através de uma instrução atômica da arquitetura.