Отладка программ в OpenMP
При написании и отладке программ в OpenMP необходимо обеспечить выполнение целого ряда специфических требований, без которых невозможно гарантировать корректность работы программы. Далее рассмотрим эти требования подробнее.
Во-первых, необходимо обеспечить, чтобы все используемые в программах библиотеки были безопасны с точки зрения потокового выполнения. При этом следует иметь в виду, что все стандартные библиотеки всегда удовлетворяют этому требованию. Таким образом, важно обеспечить выполнение этого требования только по отношению к тем библиотекам, которые создает сам разработчик или его коллеги.
Разрабатывая и отлаживая программы в OpenMP, необходимо помнить, что операции ввода/вывода в параллельных потоках завершаются в непредсказуемом порядке. Поэтому большое внимание следует уделять проблеме синхронизации параллельных процессов, помня при этом, что синхронизация - очень дорогая операция. Поэтому ею следует пользоваться как можно реже и только в случае крайней необходимости.
При разработке программ с использованием OpenMP следует внимательно прослеживать пересечение локальных переменных типа private в параллельных потоках с глобальными переменными. Кроме того, необходимо тщательно отслеживать своевременное обновление значений переменных в общей памяти. Для этого по мере необходимости следует пользоваться директивой OpenMP flush.
Проектируя программы, следует помнить, что удалить неявно установленные барьеры ( barrier ) в операторах цикла можно с помощью предложения OpenMP nowait.
Условия состязательности
При работе программ с общей памятью возникает ряд специфических ошибок, одна из которых связана с так называемыми условиями состязательности ( race conditions ). Ошибки, связанные с условиями состязательности, состоят в непредсказуемом времени завершения параллельных потоков, из-за чего возможны непредсказуемые результаты вычислений. Автоматическое распараллеливание в таких ситуациях затруднено, поскольку компилятору либо трудно, либо вообще невозможно распознать взаимозависимости по данным, возникающие в процессе параллельных вычислений при непредсказуемом времени завершения работы параллельных потоков. В таких ситуациях важно правильно определить последовательность выполнения параллельных потоков, чтобы избежать непредсказуемых результатов.
В качестве примера возникновения условия состязательности рассмотрим фрагмент программы, показанный в примере 5.1.
c$omp parallel sections A = B + C c$omp section B = A + C c$omp section C = B + A c$omp end parallel sections5.1. Фрагмент параллельной программы с возникновением условия состязательности
Результат вычислений в этом примере будет зависеть от того, в какой последовательности выполняются параллельные потоки. При этом никакой диагностики компилятора о возникновении условия состязательности в программе не выдается. Это существенно усложняет отладку параллельных приложений, если программист заранее не позаботится об исключении таких ситуаций. Устранить некорректность в вычислениях в рассматриваемом примере можно, например, с помощью синхронизации параллельных потоков или с помощью директив загрузки параллельных процессов в OpenMP. Ниже в примере 5.2 приведен пример одной из возможных модификаций фрагмента программы (пример 5.1), в котором устранены недостатки, связанные с наличием условия состязательности. В этой модификации для предотвращения условия состязательности введен счетчик событий ICOUNT и использована директива OpenMP FLUSH для организации корректного обращения к данным. Счетчик событий ICOUNT устанавливает последовательность вычислений: сначала вычисляется A=B+C, потом B=A+C и лишь затем C=B+A. Использование сочетания операторов IF и GOTO в сочетании с директивой OpenMP FLUSH позволило в этом примере провести синхронизацию трех параллельных процессов.
Еще один пример возникновения условия состязательности в программе представлен во фрагменте в примере 5.3. В этом примере условие состязательности возникает из-за неправильного в данном случае использования предложения OpenMP NOWAIT. Для корректной работы программы достаточно просто исключить предложение NOWAIT.
ICOUNT = 0 c$omp parallel sections А = В + С ICOUNT = 1 c$omp flush ICOUNT c$omp section 1000 CONTINUE c$omp flush ICOUNT IF (ICOUNT .LT. 1)GOTO 1000 B = A + C ICOUNT = 2 c$omp flush ICOUNT c$omp section 2000 CONTINUE c$omp flush ICOUNT IF (ICOUNT .LT. 2)GOTO 2000 C = B + A c$omp end parallel sections5.2. Пример фрагмента параллельной программы с устранением условия состязательности
c$omp parallel shared (X) c$omp& private (TMP) ID=omp_get_thread_num () c$omp do reduction (+ : X) DO 100 I = 1, 100 TMP = WORK(I) X = X + TMP 100 CONTINUE c$omp end do nowait Y( ID ) = X c$omp end parallel5.3. Пример возникновения условия состязательности в программе из-за неправильного использования директивы NOWAIT
В примере 5.4 условия состязательности можно избежать, описав переменную TMP в параллельной области как private.
REAL TMP, X c$omp parallel do reduction (+ : X) DO 100 I = 1, 100 TMP = WORK(I) X = X + TMP 100 CONTINUE c$omp end do Y(ID) = X c$omp end parallel5.4. Пример возникновения условия состязательности в программе из-за отсутствия описания переменной TMP как private
Мертвая блокировка
В процессе выполнения программ с общей памятью возможна ситуация, когда один из параллельных потоков ожидает освобождения доступа к объекту, который никогда не будет открыт. Такая ситуация, возникающая в параллельной программе, называется мертвой блокировкой ( deadlock ).
В качестве иллюстрации мертвой блокировки рассмотрим фрагмент программы на языке Fortran, приведенный в примере 5.5.
call omp_init_lock (lcka) call omp_init_lock (lckb) c$omp parallel sections call omp_set_lock (lcka) call omp_set_lock (lckb) call useAandB (res) call omp_unset_lock (lckb) call omp_unset_lock (lcka) c$omp section call omp_set_lock (lckb) call omp_set_lock (lcka) call useBandA (res) call omp_unset_lock (lcka) call omp_unset_lock (lckb) c$omp end parallel sections5.5. Пример возникновения мертвой блокировки в параллельной программе
В этом примере при выполнении параллельных потоков возможна ситуация, когда объект А заблокирован в одном параллельном потоке, а объект В - в другом. В результате возникает мертвая блокировка и выполнение подпрограмм useAandB и useBandA не происходит. Однако это не единственная неприятная ситуация, возникающая в этой программе.
Возможно, что при выполнении параллельных потоков в одном потоке окажутся заблокированными оба объекта A и B, тогда при выполнении рассматриваемой программы возникает условие состязательности, рассмотренное в предыдущем разделе данной лекции. Для исправления возникшей в программе ошибочной ситуации следует избавиться от различных вложенных блокировок.
Рассмотрим еще один пример мертвой блокировки, который возможен при работе программы, показанной в примере 5.6.
call omp_init_lock (LCKA) c$omp parallel sections c$omp section call omp_set_lock (LCKA) IVAL = dowork () if (IVAL .EQ. TOL) then call omp_unset_lock (LCKA) else call error (IVAL) endif c$omp section call omp_set_lock (LCKA) call use_B_and_A (RES) call omp_unset_lock (LCKA) c$omp end sections5.6. Пример возникновения мертвой блокировки в параллельной программе
В этом примере при выполнении параллельных потоков возможно возникновение как мертвой блокировки, так и условия состязательности. Мертвая блокировка возникает, если в первом параллельном потоке установлена блокировка объекта A, а IVAL не равно TOL. В этом случае установить блокировку объекта A во втором параллельном потоке невозможно, поскольку программа ждет отмены блокировки, установленной в первом параллельном потоке.
Если же в первом параллельном потоке установлена блокировка объекта A, а IVAL равно TOL, то в программе возникает условие состязательности.
Для исправления возникшей ситуации необходимо устранить не только мертвую блокировку, но и условия состязательности.