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

Загрузка и синхронизация в OpenMP

Разбить на страницы
Показывать лекцию целиком

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

Рассмотрению процессов балансировки процессов в вычислительных системах посвящено много литературы (см., например, работы [3.1-3.14]). В настоящее время можно выделить две основных группы методов решения проблем балансировки: статические и динамические. В основе статических методов лежит принцип геометрического параллелизма, когда вычислительный процесс представляется в виде взвешенного графа, а затем этот граф разбивается на эквивалентные части [3.1-3.9]. Для решения таких задач используются различные спектральные и эвристические подходы, а также их комбинации. Отметим, что для построения графов, описывающих вычислительный процесс, важно иметь и учитывать как можно больше априорной информации о процессе, поскольку в этом случае повышается точность описания.

В методах динамической балансировки априорная информация не используется. В них процессы загружаются в освободившиеся процессоры по мере освобождения последних [3.10]. При этом более равномерная загрузка процессоров получается при достаточно большом количестве независимых процессов. Решение о загрузке процессоров может приниматься по-разному: либо в одном отдельном управляющем процессе, либо в разных управляющих процессах, каждый из которых ассоциирован с вычислительным процессом. Последний подход известен также как метод коллективного решения. Этот подход развивается и разработан в Институте математического моделирования РАН [3.11]. Апробация динамической балансировки, основанной на идеях коллективного решения, показала эффективность подхода при решении различных классов вычислительных задач математической физики.

Отметим также, что в библиотеке PVM имеются средства управления процессами (Process Management), работающие в рамках системы управления ресурсами (General Resource Manager - GRM) [3.12]. Использование этих средств совместно с библиотекой PVM MPI позволяет существенно улучшить балансировку и производительность параллельных кластеров [3.13-3.14].

Синхронизация процессов в OpenMP

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

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

Во-вторых, было отмечено, что неявно (по умолчанию) синхронизация параллельных процессов обеспечивается при выполнении циклов в параллельном режиме. Была упомянута директива nowait для устранения неявной синхронизации при завершении циклов. Однако пользоваться этой директивой следует весьма и весьма аккуратно, предварительно проанализировав порядок работы программы и убедившись, что отмена синхронизации не приведет к порче данных и непредсказуемым результатам.

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

Всего в OpenMP существует шесть типов синхронизации:

  • critical,
  • atomic,
  • barrier,
  • master,
  • ordered,
  • flush.
  • Далее подробно рассмотрим эти типы.

    Синхронизация типа atomic

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

    Для задания этого типа синхронизации в OpenMP в программах, написанных на языке C/C++, используется прагма

    #pragma omp atomic 
    <операторы программы>

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

    c$omp atomic 
    <операторы программы>

    Отметим, что синхронизация atomic является альтернативой директивы reduction. Применяется эта синхронизация только для операторов, следующих непосредственно за определяющей ее директивой. Синхронизация atomic - очень дорогая операция с точки зрения трудоемкости выполнения программы. Она выполняется автоматически по умолчанию при завершении циклов в параллельном режиме. Для того чтобы ее исключить, следует использовать директиву nowait.

    Пример установки синхронизации типа atomic приведен во фрагменте программы в примере 3.1.

    integer, dimension (8) :: a, index
          data index/ 1, 1, 2, 3, 1, 4, 1, 5 /
    с$omp parallel private (I), shared (a, index)
    c$omp do
          do I =1, 8
    с$omp atomic
             a(index(I)) = a(index(I)) + index(I)
          enddo
    с$omp end parallel

    В приведенном примере в параллельном режиме синхронизируются только вычисления оператора

    a(index(I)) = a(index(I)) + index(I)

    Синхронизация типа critical

    Этот тип синхронизации используется для описания структурных блоков, выполняющихся только в одном потоке из всего набора параллельных потоков.

    Для задания синхронизации типа critical в OpenMP в программах, написанных на языке C/C++, используется прагма

    #pragma omp critical [ name ] 
    <структурный блок программы>

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

    c$omp critical [ name ] 
    < структурный блок программы > 
    c$omp end critical [ name ]

    Здесь name - имя критической секции ( critical section ). Разные критические секции независимы, если они имеют разные имена. Не поименованные критические секции относятся к одной и той же секции.

    Пример использования синхронизации типа critical приведен во фрагменте программы (пример 3.2). В этом примере определены две критических секции name1 и name2, каждая из которых выполняется только в одном из параллельных потоков. Этот тип синхронизации очень важен для достижения пиковой производительности программ.

    integer :: cnt1, cnt2
    с$omp parallel private (i)
    с$omp shared (cnt1, cnt2)
    
    с$omp do
       do i = 1, n
          ... do work ...
          if (condition1) then
    с$omp critical (name1)
             cnt1 = cnt1 + 1
    с$omp end critical (name1)
          else
    с$omp critical (name1)
             cnt1 = cnt1 - 1
    с$omp end critical (name1)
          endif
          if (condition2) then
    с$omp critical (name2)
             cnt2 = cnt2+1
    с$omp end critical (name2)
          endif
       enddo
    с$omp end parallel

    Синхронизация типа barrier

    Синхронизация типа barrier устанавливает режим ожидания завершения работы всех запущенных в программе параллельных потоков при достижении точки barrier.

    Для задания синхронизации типа barrier в OpenMP в программах, написанных на языке C/C++, используется прагма

    #pragma omp barrier

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

    c$ omp barrier

    Пример использования синхронизации типа barrier приведен во фрагменте программы (пример 3.3). В этом примере синхронизируется выполнение блока операторов <assignment>. Следующий блок <dependent work> начинает свою работу во всех параллельных потоках лишь только после того, как во всех параллельных потоках будут завершено выполнение блока <assignment>.

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

    с$omp parallel
    с$omp do
          do i = 1, n
             <assignment>
    с$omp barrier
             <dependent work>
          enddo
    с$omp end parallel

    Здесь же отметим, что на языке Fortran определение директивы nowait выглядит так:

    c$omp end do nowait

    а на языке C/C++ - так:

    #pragma omp for nowait

    Синхронизация типа master

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

    #pragma omp master

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

    c$omp master 
    <структурный блок> 
    c$omp end master

    В примере 3.4 приведен пример фрагмента программы, иллюстрирующий использование синхронизации типа master. В этом примере операторы print и read выполняются исключительно в главном потоке.

    ! $omp parallel shared (c, scale)
    ! $omp private (j, myid)
          myid = omp_get_thread_num ( )
    ! $omp master
          print *, 'T: ', myid, ' enter scale'
          read *, scale
    ! $omp end master
    ! $omp barrier
    ! $omp do
          do j = 1, N
             c (j) = scale * c (j)
          enddo
    ! $omp end do
    ! $omp end parallel

    Синхронизация типа ordered

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

    с$omp parallel default (shared) private (I, J)
    с$omp do ordered
          do I = 1, N
             do J = 1, M
                Z(I) = Z(I) + X (I, J) * Y(J, I)
             enddo
    с$omp ordered
             if (I<21) then
                print*, 'Z (',I,') = ', Z (I)
             endif
    с$omp end ordered
          enddo

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

    Z (1) = 1007.167786
    Z (2) = 1032.933350
    Z (3) = 1033.125610
    Z (4) = 1009.944641
    Z (5) = 1016.547302
    Z (6) = 1005.789124
    Z (7) = 1025.048584
    Z (8) = 1003.904358
    Z (9) = 995.5405273
    Z (10) = 991.2892456
    Z (11) = 1011.334167
    Z (12) = 1010.631897
    Z (13) = 1009.581848
    Z (14) = 976.2397461
    Z (15) = 978.1119385
    Z (16) = 977.7111816
    Z (17) = 971.2011719
    Z (18) = 998.4275513
    Z (19) = 1018.487000
    Z (20) = 978.0640259

    В программах на языке C/C++ синхронизация типа ordered описывается следующим образом:

    #pragma omp ordered 
    <структурный блок>

    а в программах, написанных на языке Fortran, синхронизация типа ordered устанавливается так:

    c$omp ordered 
    <структурный блок> 
    c$omp end ordered

    В примере 3.5 приведен пример фрагмента программы, иллюстрирующий применение синхронизации типа ordered.

    Видно, что в приведенном примере вывод результатов элементов массива Z осуществляется в порядке возрастания индексов элементов массива, как в обычной последовательной программе.

    Синхронизация типа flush

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

    В программах на языке C/C++ синхронизация типа flush описывается следующим образом:

    #pragma omp flush(var1, [var2, [..., varN ]])

    На языке Fortran эта директива синхронизации типа flush выглядит так:

    c$omp flush var1, [var2, [..., varN ]])

    Здесь var1, var2, ..., varN - список переменных, значения которых сохраняются в оперативной памяти в момент выполнения директории flush.

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

    program flush
            integer, parameter :: M=1600000
            integer, dimension (M) :: c
            integer :: stop, sum, tid
            integer, dimension (0:1) :: done
            integer, external :: omp_get_thread_num
    
            call omp_set_num_threads (2)
            c = 1
            c (345) = 9
    !$omp parallel default (private) shared (done, c, stop)
            tid=omp_get_ thread_num ( )
            done (tid) = 0
            if (tid == 0) then
               neigh = 1
            else
               neigh = 0
            end if
    !$omp barrier
            if (tid == 0) then
               do j = 1, M
                  if (c j) == 9) stop = j
               enddo
            endif
            done (tid) = 1
    !$omp flush (done)
            do while (done(neigh) .eq. 0)
    !$omp flush (done)
            enddo
            if (tid == 1) then
               sum=0
               do j = 1, stop - 1
                  sum = sum + c (j)
               enddo
            endif
    !$omp end parallel
          end program flush

    Загрузка процессов в OpenMP. Директива schedule

    Проблема загрузки параллельных потоков является важной проблемой не только для параллельного программирования с использованием OpenMP, но и для всего параллельного программирования в целом. Эта проблема тесно связана с проблемой балансировки загрузки процессоров параллельных высокопроизводительных вычислительных систем, а также с проблемой повышения эффективности работы параллельных программ. Понятно, что для высокопроизводительной параллельной вычислительной системы успешное решение проблемы балансировки загрузки процессоров является ключом к решению задачи повышения эффективности вычислительной системы в целом. Как известно, процессы по-разному могут использовать вычислительные возможности процессоров. Так, на стадии интенсивных арифметических вычислений коэффициент загрузки процессоров обычно близок к 100%. На стадии интенсивных операций ввода/вывода коэффициент загрузки процессоров меняется в диапазоне от нескольких процентов до десятков процентов. Кроме того, процессоры могут простаивать и в том случа е, когда процессы, переданные им, уже обработаны, а другие процессоры все еще продолжают обрабатывать процессы той же задачи. Для исключения простоев или уменьшения времени простоев применяют различные методы балансировки процессов. Существующие в OpenMP различные методы загрузки процессов также могут быть применены для улучшения балансировки работы параллельных вычислительных систем.

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

    schedule( type [ , chunk ] )

    Здесь type - параметр, определяющий тип загрузки, а chunk - параметр, который определяет порции данных, пересылаемых между процессами (по умолчанию значение параметра chunk равно 1 ).

    В OpenMP параметр type принимает одно из следующих значений:

  • static,
  • dynamic,
  • guided,
  • runtime.
  • Загрузка типа static

    В этом случае вся совокупность загружаемых процессов разбивается на равные порции размера chunk, и эти порции последовательно распределяются между процессорами (или потоками, которые затем и выполняются на этих процессорах) с первого до последнего и т. д.

    В качестве примера использования загрузки типа static рассмотрим фрагмент программы на языке Fortran, приведенный в примере 3.7.

    с$omp do shared (x) private (i)
    с$omp schedule (static, 1000)
          do i = 1, 12000
             ... work ...
          enddo

    Схема распределения загрузки процессов по процессорам (или потокам ( threads ), которые затем выполняются на процессорах), соответствующая этому примеру, приведена на рис.3.1. Как видно из этой схемы, все 12000 процессов разбиты на 12 порций по 1000 процессов ( chunk=1000 ). Загрузка порций в потоки ( threads ) происходит последовательно. Сначала порции в порядке нумерации загружаются в первый, второй, третий и четвертый потоки, а затем загрузка производится опять в том же порядке, начиная с первого потока и т. д.

    (рис 3.1) Схема загрузки процессоров для программы, приведенной в примере 3.7

    Загрузка типа dynamic

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

    Пример применения загрузки типа dynamic приведен в следующем фрагменте программы (пример 3.8).

    с$omp do shared (x) private (i)
    с$omp schedule (dynamic, 1000)
          do i = 1, 10000
             ... work ...
          enddo

    Загрузка типа guided

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

    Пример применения загрузки типа guided приведен в следующем фрагменте программы, изображенном на пример 3.9.

    с$omp do shared (x) private (i)
    с$omp schedule (guided, 55)
          do i = 1, 12000
             ... work ...
          enddo

    В этом режиме обеспечивается достаточно сбалансированная загрузка потоков с небольшими задержками при завершении параллельной обработки.

    Загрузка типа runtime

    В этом случае загрузка порций процессов по потокам (или процессорам) определяется значением переменной окружения OMP_SCHEDULE. Значение этой переменной проверяется перед каждой загрузкой процессов в потоки во время работы программы. По умолчанию оно static.

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

    $ setenv OMP_SCHEDULE static,1000 
    $ setenv OMP_SCHEDULE dynamic

    В первом примере через значение переменной окружения в качестве загрузки runtime задается режим static с размером порции равным 1000. Во втором же примере в качестве загрузки runtime задается режим dynamic с размером порции, по умолчанию заданным равным 1.

    Страницы:

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

    Рассмотрению процессов балансировки процессов в вычислительных системах посвящено много литературы (см., например, работы [3.1-3.14]). В настоящее время можно выделить две основных группы методов решения проблем балансировки: статические и динамические. В основе статических методов лежит принцип геометрического параллелизма, когда вычислительный процесс представляется в виде взвешенного графа, а затем этот граф разбивается на эквивалентные части [3.1-3.9]. Для решения таких задач используются различные спектральные и эвристические подходы, а также их комбинации. Отметим, что для построения графов, описывающих вычислительный процесс, важно иметь и учитывать как можно больше априорной информации о процессе, поскольку в этом случае повышается точность описания.

    В методах динамической балансировки априорная информация не используется. В них процессы загружаются в освободившиеся процессоры по мере освобождения последних [3.10]. При этом более равномерная загрузка процессоров получается при достаточно большом количестве независимых процессов. Решение о загрузке процессоров может приниматься по-разному: либо в одном отдельном управляющем процессе, либо в разных управляющих процессах, каждый из которых ассоциирован с вычислительным процессом. Последний подход известен также как метод коллективного решения. Этот подход развивается и разработан в Институте математического моделирования РАН [3.11]. Апробация динамической балансировки, основанной на идеях коллективного решения, показала эффективность подхода при решении различных классов вычислительных задач математической физики.

    Отметим также, что в библиотеке PVM имеются средства управления процессами (Process Management), работающие в рамках системы управления ресурсами (General Resource Manager - GRM) [3.12]. Использование этих средств совместно с библиотекой PVM MPI позволяет существенно улучшить балансировку и производительность параллельных кластеров [3.13-3.14].

    Синхронизация процессов в OpenMP

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

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

    Во-вторых, было отмечено, что неявно (по умолчанию) синхронизация параллельных процессов обеспечивается при выполнении циклов в параллельном режиме. Была упомянута директива nowait для устранения неявной синхронизации при завершении циклов. Однако пользоваться этой директивой следует весьма и весьма аккуратно, предварительно проанализировав порядок работы программы и убедившись, что отмена синхронизации не приведет к порче данных и непредсказуемым результатам.

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

    Всего в OpenMP существует шесть типов синхронизации:

  • critical,
  • atomic,
  • barrier,
  • master,
  • ordered,
  • flush.
  • Далее подробно рассмотрим эти типы.

    Синхронизация типа atomic

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

    Для задания этого типа синхронизации в OpenMP в программах, написанных на языке C/C++, используется прагма

    #pragma omp atomic 
    <операторы программы>

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

    c$omp atomic 
    <операторы программы>

    Отметим, что синхронизация atomic является альтернативой директивы reduction. Применяется эта синхронизация только для операторов, следующих непосредственно за определяющей ее директивой. Синхронизация atomic - очень дорогая операция с точки зрения трудоемкости выполнения программы. Она выполняется автоматически по умолчанию при завершении циклов в параллельном режиме. Для того чтобы ее исключить, следует использовать директиву nowait.

    Пример установки синхронизации типа atomic приведен во фрагменте программы в примере 3.1.

    integer, dimension (8) :: a, index
          data index/ 1, 1, 2, 3, 1, 4, 1, 5 /
    с$omp parallel private (I), shared (a, index)
    c$omp do
          do I =1, 8
    с$omp atomic
             a(index(I)) = a(index(I)) + index(I)
          enddo
    с$omp end parallel

    В приведенном примере в параллельном режиме синхронизируются только вычисления оператора

    a(index(I)) = a(index(I)) + index(I)

    Синхронизация типа critical

    Этот тип синхронизации используется для описания структурных блоков, выполняющихся только в одном потоке из всего набора параллельных потоков.

    Для задания синхронизации типа critical в OpenMP в программах, написанных на языке C/C++, используется прагма

    #pragma omp critical [ name ] 
    <структурный блок программы>

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

    c$omp critical [ name ] 
    < структурный блок программы > 
    c$omp end critical [ name ]

    Здесь name - имя критической секции ( critical section ). Разные критические секции независимы, если они имеют разные имена. Не поименованные критические секции относятся к одной и той же секции.

    Пример использования синхронизации типа critical приведен во фрагменте программы (пример 3.2). В этом примере определены две критических секции name1 и name2, каждая из которых выполняется только в одном из параллельных потоков. Этот тип синхронизации очень важен для достижения пиковой производительности программ.

    integer :: cnt1, cnt2
    с$omp parallel private (i)
    с$omp shared (cnt1, cnt2)
    
    с$omp do
       do i = 1, n
          ... do work ...
          if (condition1) then
    с$omp critical (name1)
             cnt1 = cnt1 + 1
    с$omp end critical (name1)
          else
    с$omp critical (name1)
             cnt1 = cnt1 - 1
    с$omp end critical (name1)
          endif
          if (condition2) then
    с$omp critical (name2)
             cnt2 = cnt2+1
    с$omp end critical (name2)
          endif
       enddo
    с$omp end parallel

    Синхронизация типа barrier

    Синхронизация типа barrier устанавливает режим ожидания завершения работы всех запущенных в программе параллельных потоков при достижении точки barrier.

    Для задания синхронизации типа barrier в OpenMP в программах, написанных на языке C/C++, используется прагма

    #pragma omp barrier

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

    c$ omp barrier

    Пример использования синхронизации типа barrier приведен во фрагменте программы (пример 3.3). В этом примере синхронизируется выполнение блока операторов <assignment>. Следующий блок <dependent work> начинает свою работу во всех параллельных потоках лишь только после того, как во всех параллельных потоках будут завершено выполнение блока <assignment>.

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

    с$omp parallel
    с$omp do
          do i = 1, n
             <assignment>
    с$omp barrier
             <dependent work>
          enddo
    с$omp end parallel

    Здесь же отметим, что на языке Fortran определение директивы nowait выглядит так:

    c$omp end do nowait

    а на языке C/C++ - так:

    #pragma omp for nowait

    Синхронизация типа master

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

    #pragma omp master

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

    c$omp master 
    <структурный блок> 
    c$omp end master

    В примере 3.4 приведен пример фрагмента программы, иллюстрирующий использование синхронизации типа master. В этом примере операторы print и read выполняются исключительно в главном потоке.

    ! $omp parallel shared (c, scale)
    ! $omp private (j, myid)
          myid = omp_get_thread_num ( )
    ! $omp master
          print *, 'T: ', myid, ' enter scale'
          read *, scale
    ! $omp end master
    ! $omp barrier
    ! $omp do
          do j = 1, N
             c (j) = scale * c (j)
          enddo
    ! $omp end do
    ! $omp end parallel

    Синхронизация типа ordered

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

    с$omp parallel default (shared) private (I, J)
    с$omp do ordered
          do I = 1, N
             do J = 1, M
                Z(I) = Z(I) + X (I, J) * Y(J, I)
             enddo
    с$omp ordered
             if (I<21) then
                print*, 'Z (',I,') = ', Z (I)
             endif
    с$omp end ordered
          enddo

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

    Z (1) = 1007.167786
    Z (2) = 1032.933350
    Z (3) = 1033.125610
    Z (4) = 1009.944641
    Z (5) = 1016.547302
    Z (6) = 1005.789124
    Z (7) = 1025.048584
    Z (8) = 1003.904358
    Z (9) = 995.5405273
    Z (10) = 991.2892456
    Z (11) = 1011.334167
    Z (12) = 1010.631897
    Z (13) = 1009.581848
    Z (14) = 976.2397461
    Z (15) = 978.1119385
    Z (16) = 977.7111816
    Z (17) = 971.2011719
    Z (18) = 998.4275513
    Z (19) = 1018.487000
    Z (20) = 978.0640259

    В программах на языке C/C++ синхронизация типа ordered описывается следующим образом:

    #pragma omp ordered 
    <структурный блок>

    а в программах, написанных на языке Fortran, синхронизация типа ordered устанавливается так:

    c$omp ordered 
    <структурный блок> 
    c$omp end ordered

    В примере 3.5 приведен пример фрагмента программы, иллюстрирующий применение синхронизации типа ordered.

    Видно, что в приведенном примере вывод результатов элементов массива Z осуществляется в порядке возрастания индексов элементов массива, как в обычной последовательной программе.

    Синхронизация типа flush

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

    В программах на языке C/C++ синхронизация типа flush описывается следующим образом:

    #pragma omp flush(var1, [var2, [..., varN ]])

    На языке Fortran эта директива синхронизации типа flush выглядит так:

    c$omp flush var1, [var2, [..., varN ]])

    Здесь var1, var2, ..., varN - список переменных, значения которых сохраняются в оперативной памяти в момент выполнения директории flush.

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

    program flush
            integer, parameter :: M=1600000
            integer, dimension (M) :: c
            integer :: stop, sum, tid
            integer, dimension (0:1) :: done
            integer, external :: omp_get_thread_num
    
            call omp_set_num_threads (2)
            c = 1
            c (345) = 9
    !$omp parallel default (private) shared (done, c, stop)
            tid=omp_get_ thread_num ( )
            done (tid) = 0
            if (tid == 0) then
               neigh = 1
            else
               neigh = 0
            end if
    !$omp barrier
            if (tid == 0) then
               do j = 1, M
                  if (c j) == 9) stop = j
               enddo
            endif
            done (tid) = 1
    !$omp flush (done)
            do while (done(neigh) .eq. 0)
    !$omp flush (done)
            enddo
            if (tid == 1) then
               sum=0
               do j = 1, stop - 1
                  sum = sum + c (j)
               enddo
            endif
    !$omp end parallel
          end program flush

    Загрузка процессов в OpenMP. Директива schedule

    Проблема загрузки параллельных потоков является важной проблемой не только для параллельного программирования с использованием OpenMP, но и для всего параллельного программирования в целом. Эта проблема тесно связана с проблемой балансировки загрузки процессоров параллельных высокопроизводительных вычислительных систем, а также с проблемой повышения эффективности работы параллельных программ. Понятно, что для высокопроизводительной параллельной вычислительной системы успешное решение проблемы балансировки загрузки процессоров является ключом к решению задачи повышения эффективности вычислительной системы в целом. Как известно, процессы по-разному могут использовать вычислительные возможности процессоров. Так, на стадии интенсивных арифметических вычислений коэффициент загрузки процессоров обычно близок к 100%. На стадии интенсивных операций ввода/вывода коэффициент загрузки процессоров меняется в диапазоне от нескольких процентов до десятков процентов. Кроме того, процессоры могут простаивать и в том случа е, когда процессы, переданные им, уже обработаны, а другие процессоры все еще продолжают обрабатывать процессы той же задачи. Для исключения простоев или уменьшения времени простоев применяют различные методы балансировки процессов. Существующие в OpenMP различные методы загрузки процессов также могут быть применены для улучшения балансировки работы параллельных вычислительных систем.

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

    schedule( type [ , chunk ] )

    Здесь type - параметр, определяющий тип загрузки, а chunk - параметр, который определяет порции данных, пересылаемых между процессами (по умолчанию значение параметра chunk равно 1 ).

    В OpenMP параметр type принимает одно из следующих значений:

  • static,
  • dynamic,
  • guided,
  • runtime.
  • Загрузка типа static

    В этом случае вся совокупность загружаемых процессов разбивается на равные порции размера chunk, и эти порции последовательно распределяются между процессорами (или потоками, которые затем и выполняются на этих процессорах) с первого до последнего и т. д.

    В качестве примера использования загрузки типа static рассмотрим фрагмент программы на языке Fortran, приведенный в примере 3.7.

    с$omp do shared (x) private (i)
    с$omp schedule (static, 1000)
          do i = 1, 12000
             ... work ...
          enddo

    Схема распределения загрузки процессов по процессорам (или потокам ( threads ), которые затем выполняются на процессорах), соответствующая этому примеру, приведена на рис.3.1. Как видно из этой схемы, все 12000 процессов разбиты на 12 порций по 1000 процессов ( chunk=1000 ). Загрузка порций в потоки ( threads ) происходит последовательно. Сначала порции в порядке нумерации загружаются в первый, второй, третий и четвертый потоки, а затем загрузка производится опять в том же порядке, начиная с первого потока и т. д.

    (рис 3.1) Схема загрузки процессоров для программы, приведенной в примере 3.7

    Загрузка типа dynamic

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

    Пример применения загрузки типа dynamic приведен в следующем фрагменте программы (пример 3.8).

    с$omp do shared (x) private (i)
    с$omp schedule (dynamic, 1000)
          do i = 1, 10000
             ... work ...
          enddo

    Загрузка типа guided

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

    Пример применения загрузки типа guided приведен в следующем фрагменте программы, изображенном на пример 3.9.

    с$omp do shared (x) private (i)
    с$omp schedule (guided, 55)
          do i = 1, 12000
             ... work ...
          enddo

    В этом режиме обеспечивается достаточно сбалансированная загрузка потоков с небольшими задержками при завершении параллельной обработки.

    Загрузка типа runtime

    В этом случае загрузка порций процессов по потокам (или процессорам) определяется значением переменной окружения OMP_SCHEDULE. Значение этой переменной проверяется перед каждой загрузкой процессов в потоки во время работы программы. По умолчанию оно static.

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

    $ setenv OMP_SCHEDULE static,1000 
    $ setenv OMP_SCHEDULE dynamic

    В первом примере через значение переменной окружения в качестве загрузки runtime задается режим static с размером порции равным 1000. Во втором же примере в качестве загрузки runtime задается режим dynamic с размером порции, по умолчанию заданным равным 1.

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