Параллельное программирование с использованием OpenMP

Дополнительные возможности OpenMP

Показывать лекцию целиком

Задание переменных окружения с помощью функций runtime OpenMP

В предыдущих лекциях уже затрагивались вопросы задания переменных окружения, определяющих режимы работы параллельных частей программ. Было показано, что для задания их значений можно воспользоваться командами операционной системы Linux или директивами OpenMP. Однако в OpenMP существует еще одна возможность задания переменных окружения: с помощью функций библиотеки runtime OpenMP. Обращение к этим функциям осуществляется непосредственно в программе и ничем не отличается от вызова обычных функций. Однако следует иметь в виду, что подобная возможность существует не во всех реализациях OpenMP. Так, в реализациях OpenMP, поддерживаемых компанией Silicon Graphics, такая возможность отсутствует. Это же замечание относится и к реализациям OpenMP на многопроцессорных вычислительных системах G-Scale s-350 компании Kraftway.

В программах, написанных на C/C++ с использованием OpenMP, существуют следующие возможности задания переменных окружения с помощью средств библиотеки реального времени runtime OpenMP.

С помощью вызова функции

(void) omp_set_num_threads(int num_threads)

можно задать число потоков в области параллельных вычислений, т. е. определить значение переменной окружения OMP_NUM_THREADS. При завершении работы эта функция не возвращает в программу никаких значений.

Функция

int omp_get_num_threads()

напротив, возвращает в программу целочисленное значение, равное количеству параллельных потоков в текущий момент времени.

Следующая функция

int omp_get_max_threads()

возвращает в программу целочисленное значение, равное максимальному количеству параллельных потоков, которое может быть возвращено функцией omp_get_num_threads.

Для определения номера параллельного потока в текущий момент времени программы можно воспользоваться функцией

int_omp_get_thread_num()

Она возвращает целочисленное значение в диапазоне от 0 до OMP_NUM_THREADS-1.

Определить количество процессоров, доступных программе в текущей точке, можно с помощью функции

int omp_get_num_procs()

Следующая функция позволяет идентифицировать, в какой области программы (параллельной или последовательной) в текущий момент времени проводятся вычисления

(int/logical) omp_in_parallel()

Она возвращает значение TRUE или 1 в точке параллельной области программы и FALSE или 0 в точке последовательной области программы.

Задать или отменить динамический режим работы программы можно, воспользовавшись функцией

(void) omp_set_dynamic( TRUE | FALSE )

Для задания динамического режима следует использовать параметр TRUE, а для его отмены - параметр FALSE. При этом в списке переменных окружения определяется или удаляется переменная окружения OMP_DYNAMIC или задается ее значение, соответственно равное TRUE или FALSE.

Следующая функция позволяет идентифицировать, какой режим работы параллельной части программы (динамический или статический) осуществляется в текущий момент времени при вызове функции

(int/logical) omp_get_dynamic()

Эта функция возвращает значение TRUE или 1, если режим динамический, и FALSE или 0 в случае статического режима.

Задать или отменить вложенный режим параллельной обработки процессов в параллельной области программы можно с помощью функции

(void) omp_set_nested( TRUE | FALSE )

Она устанавливает или отменяет вложенный режим параллельной обработки. При этом в списке переменных окружения определяется или удаляется переменная окружения OMP_NESTED либо задается ее значение, соответственно равное TRUE или FALSE.

Следующая функция позволяет идентифицировать, установлен или нет вложенный режим параллельной обработки в момент вызова функции

(int/logical) omp_get_nested()

Эта функция возвращает значение TRUE или 1 для вложенного режима параллельной обработки и значение FALSE или 0 при отсутствии такового.

При обращении ко всем вышеперечисленным функциям в заголовки программ на языке С/С++ необходимо включать строку:

#include <omp.h>

В программах, написанных на языке Fortran, существуют одноименные аналогичные функции. При этом функции, возвращающие значения, реализованы в виде функций, а функции, устанавливающие значения переменных окружения и не возвращающие никаких значений, реализованы в виде подпрограмм. Тип функций в программах, написанных на языке Fortran, должен быть определен в соответствии с типом возвращаемых данных.

Передача данных с помощью директивы threadprivate

Проблема передачи данных в параллельных программах, написанных с использованием OpenMP, уже рассматривалась в предыдущих разделах. Однако был изложен лишь вариант передачи (миграции) данных из главного потока программы в параллельные. В этом же разделе мы остановимся на проблеме передачи данных между параллельными потоками из одного параллельного структурного блока программы в другой, минуя промежуточный последовательный структурный блок. Для реализации такого механизма передачи данных в OpenMP имеется специальная директива threadprivate.

Директива threadprivate применяется к глобальным переменным программы, которые нужно сделать локальными переменными для каждого из параллельных потоков. Кроме того, эта директива позволяет передавать локальные данные из одного параллельного структурного блока в другой, а также сохранять локальные данные из параллельных потоков на протяжении всей программы. В программах, написанных на языке Fortran, для этого необходимо поместить переменные в common-блок, а затем описать этот блок с помощью директивы threadprivate следующим образом:

c$omp threadprivate (/cb1/, [/cb2/ [ , ..., /cbN/]]

Здесь cb1, cb2, ..., cbN - имена common -блоков, для которых создаются локальные копии во всех параллельных потоках. В результате с первой же параллельной области, следующей за директивой threadprivate, будут созданы локальные копии common -блоков. Важно отметить, что в процессе передачи данных сохраняется соответствие между значениями передаваемых переменных и номерами параллельных потоков.

Пример фрагмента программы, написанной на языке Fortran с использованием директивы OpenMP threadprivate, приведен в примере 4.1.

Как видно из этого примера, для передачи данных из одного параллельного структурного блока в другой в данном примере используется переменная x, определенная в common-блоке mine. Имя этого common-блока описано с помощью директивы threadprivate. В первом параллельном структурном блоке программы вычисляются и выводятся на печать значения переменной x в каждом из четырех параллельных потоков. После завершения первого параллельного блока значения этой переменной выводятся в следующем последовательном блоке программы. Отметим, что в этом случае напечатанное значение соответствует главному потоку программы (его индекс tid всегда равен 0 ), в который передает свое значение x первый параллельный поток с индексом tid, равным 0.

Во втором параллельном структурном блоке значения переменной x не изменяются и выводятся на печать в точном соответствии с индексами параллельных потоков tid, установленных в первом параллельном блоке с индексом tid=0.

В программах, написанных на языке Fortran, предложение threadprivate имеет следующий вид:

program region
         integer, external :: omp_get_thread_num
         integer :: tid, x
         common/mine/ x
!$omp threadprivate (/mine/)
         call omp_set_num_threads (4)
!$omp parallel private (tid)
         tid = omp_get__thread_num ( )
         x = tid * 10 + 1
         print *,"T:", tid," inside first parallel region x =",x
!$omp end parallel
         print *,"T:", tid," outside parallel region x =",x
!$omp parallel private (tid)
         tid = omp_get_thread_num( )
         print *,"T:", tid," inside next parallel region x =",x
!$omp end parallel
      end program region

Результаты работы программы:

T: 0 inside first parallel region x = 1
T: 1 inside first parallel region x = 11
T: 2 inside first parallel region x = 21
T: 3 inside first parallel region x = 31
T: 0 outside parallel region      x = 1
T: 0 inside next parallel region  x = 1
T: 2 inside next parallel region  x = 21
T: 3 inside next parallel region  x = 31
T: 1 inside next parallel region  x = 11

Здесь cb1, cb2, ..., cbN - список переменных, для которых создаются локальные копии во всех параллельных потоках.

Отметим, что элементами списка не могут быть ссылки или переменные, не полностью определенные. Кроме того, адреса констант также недопустимы в списке.

Все переменные, включенные в список директив threadprivate, должны быть проинициализированы до начала первого параллельного блока, и их инициализация возможна только один раз на протяжении всей программы. Кроме того, переменные, включенные в список, не могут фигурировать в других предложениях OpenMP, за исключением copyin, schedule или if. Категорически запрещено их использование в директивах private, firstprivate, lastprivate, shared и reduction.

Не допускается применение передачи значений переменных из одного параллельного структурного блока в другой в динамическом ( dynamic ) режиме работы параллельной программы. На всем протяжении действия директивы threadprivate должен быть установлен статический режим работы параллельной программы.

В примере 4.2 приведен пример фрагмента параллельной программы, написанной на языке C/C++ с использованием директивы OpenMP threadprivate. В этом примере для отмены динамического режима явно вызывается функция из библиотеки реального времени runtime OpenMP omp_set_dynamic с параметром 0.

#include <omp.h>

int alpha [10], beta[10], i

#pragma omp threadprivate (alpha)

main ( )
{
/*Выключение динамического режима*/

      omp_set_dynamic (0);

/*1-й параллельный блок */

#pragma omp parallel private (i, beta)
      for (i=0; i<10; i++)
         alpha[i] = beta[i] = i;

/*2-й параллельный блок*/

#pragma omp parallel
      printf ("alpha[3]=%d and beta [3] =%d\n", alpha[3], beta[3]);
}

Устаревшая конструкция передачи данных в директиве parallel do в OpenMP

В OpenMP имеется возможность передачи данных из главного потока в параллельные и назад через вызов функции в параллельном режиме. Эта конструкция считается устаревшей и в настоящее время редко используется на практике. Однако далее для полноты изложения все же рассмотрим возможность применения этой конструкции в программах с OpenMP.

Пример параллельной программы, написанной на языке Fortran с использованием вызова функции для передачи данных в параллельные потоки, приведен в примере 4.3.

program orphan
         integer, parameter :: M=8
         integer, dimension(M) :: x
         integer :: myid, i
         common/global/ x, myid, i
         call omp_set_num_threads (4)
!$omp parallel shared (x)
         call work ( )
!$omp end parallel
         write (6, *) x
      end program orphan
         
      subroutine work ( )
         integer, parameter :: M=8
         integer, dimension (M) :: x
         integer, external :: omp_get_thread_num
         common/global/ x, myid, i
!$omp do private (i, myid) ordered
         do i = 1. M
            myid = omp_get_thread_num ( )
            write (6, *) "T:", myid, " i=", i
            x (i) = myid
         enddo
!$omp end do
         return
      end subroutine work

В этом примере в управляющей программе orphan определяется число параллельных потоков OMP_NUM_THREADS=4. Затем в параллельном режиме осуществляется обращение к подпрограмме work. В этой подпрограмме цикл do i=1,M при M=8 выполняется в параллельном режиме так: 8 петель этого цикла распределяются в последовательном порядке согласно директиве orderedпо потокам и потоки выполняются в последовательном порядке. В конце программы в последовательном структурном блоке осуществляется вывод элементов массива x. Поэтому результат работы этой программы будет таким, как показано в примере 4.4.

Т: 	  0 i=    1
Т: 	  0 i=    2
Т: 	  1 i=    3
Т: 	  1 i=    4
Т: 	  2 i=    5
Т: 	  2 i=    6
Т: 	  3 i=    7
Т: 	  3 i=    8
  0       0   1   1    2    2
  3   3

Почему же такая конструкция редко применяется на практике? Дело в том, что здесь в параллельной области программы вызывается подпрограмма work, а обращение к подпрограмме имеет почти такую же трудоемкость, как и открытие параллельных потоков. Поэтому вместо вызова подпрограмм или функций желательно подставлять их текст в определенное место программы, т. е. делать inline -подстановку. В противном случае зачастую можно получить неэффективную параллельную программу.

Функции блокировки в OpenMP

Функции блокировки играют очень важную роль при написании параллельных программ. Для последовательных программ необходимость функций блокировки обусловлена в основном многопользовательским режимом, когда на одном процессоре одновременно могут выполняться несколько заданий различных пользователей. В такой ситуации очень важно обеспечить корректный доступ к данным и их корректную модификацию со стороны различных задач или пользователей. В параллельном программировании даже одно задание порождает несколько параллельных процессов, каждый из которых может оперировать с одними и теми же данными. Для обеспечения корректности доступа к данным и их корректной модификации в OpenMP существуют специальные функции блокировки, обеспечивающие решение задач корректности доступа, записи и обновления данных.

Сначала рассмотрим функции блокировки в OpenMP в программах, написанных на языке C/C++.

Функция

void omp_init_lock ( omp_lock_t *lock )

предназначена для инициализации блокировки объекта с указателем lock. Она не возвращает никаких значений.

Следующая функция

void omp_destroy_lock ( omp_lock_t *lock )

гарантирует, что объект с указателем lock в данный момент не инициализирован.

С помощью функции

void omp_set_lock ( omp_lock_t *lock )

можно организовать блокировку выполнения потока до тех пор, пока объект открыт для операций чтения-записи. После завершения операций чтения-записи объект разблокируется и продолжается выполнение потока.

Для открытия заблокированного объекта с указателем lock можно воспользоваться функцией

void omp_unset_lock ( omp_lock_t *lock )

Функция

int omp_test_lock ( omp_lock_t *lock )

позволяет попытаться заблокировать объект, не прерывая выполнения потока. Она возвращает значение TRUE или 1, если попытка завершилась успешно. В противном случае возвращается значение FALSE или 0.

Напомним, что при обращении к функциям блокировки в заголовке текста программы должно содержаться включение описания этих функций:

#include < omp.h >

В примере 4.5 приведен пример фрагмента программы на языке C/C++, иллюстрирующий применение функций блокировки.

#include <omp.h>
void main ( )
{
     omp_lock_t lock;
     int myid;
     omp_init_lock (lock);
#pragma omp parallel shared(lock) private(myid)
     {
        myid = omp_get_thread_num ( );
        omp_set_lock (lock);
        printf ("Hello from thread %d\n", myid);
        omp_unset_lock (lock);
        while (! omp_test_lock (lock))
        {
            skip (myid);
        }
        do_work (myid);
        omp_unset_lock (lock);
     }
     omp_destroy_lock (lock);
}

В программах, написанных на языке Fortran, переменная lock должна быть целочисленной типа KIND и иметь размерность, достаточную для записи адреса.

Подпрограмма

subroutine omp_init_lock ( omp_lock_t lock )

как и в языке C/C++, используется для инициализации блокировки объекта с указателем lock.

Следующая подпрограмма

subroutine omp_destroy_lock ( omp_lock_t lock )

обеспечивает отсутствие блокировки объекта с указателем lock в данный момент времени.

Подпрограмма

subroutine omp_set_lock ( omp_lock_t lock )

устанавливает блокировку выполнения потока до тех пор, пока объект открыт для операций чтения-записи. После завершения операций чтениязаписи объект блокируется и продолжается выполнение потока.

С помощью подпрограммы можно открыть заблокированный объект с указателем lock:

subroutine omp_unset_lock ( omp_lock_t lock )

Функция

logical omp_test_lock ( omp_lock_t *lock )

как и в языке C/C++, позволяет попытаться заблокировать объект, не прерывая выполнения потока. Она возвращает значение TRUE, если попытка завершилась успешно. В противном случае возвращается значение FALSE.

Вернуться к учебному плану