Параллельное программирование с использованием OpenMP
7.5. Организация взаимоисключения при использовании общих переменных
Как уже неоднократно отмечалось ранее, при изменении общих данных несколькими потоками должны быть обеспечены условия взаимоисключения - изменение значений общих переменных должно осуществляться в каждый конкретный момент времени только одним потоком. Рассмотрим возможные способы организации взаимоисключения.
7.5.1. Обеспечение атомарности (неделимости) операций
Действие над общей переменной может быть выполнено как атомарная (неделимая) операция при помощи директивы atomic. Формат директивы имеет вид:
#pragma omp atomic <expression>
где expression должно иметь вид:
x++ ; или ++x ; или x-- ; или --x;
где x есть имя любой целой скалярной переменной.
Директива atomic может быть записана и в виде:
#pragma omp atomic x <operator>= <expression>
где x есть имя скалярной переменной, выражение expression не должно включать переменную x, а допустимыми значениями для поля operator являются следующие операции (которые не могут быть перегружены):
+, *, -, /, &, |, ^, >>, <<
Как следует из названия, операция директивы atomic выполняется как неделимое действие над указанной общей переменной, и, как результат, никакие другие потоки не могут получить доступ к этой переменной в этот момент времени.
Применим рассмотренную директиву для нашей учебной задачи при вычислении общей суммы:
total = 0; #pragma omp parallel for shared(a) private(i,j,sum) { for (i=0; i < NMAX; i++) { sum = 0; for (j=i; j < NMAX; j++) sum += a[i][j]; printf ("Сумма элементов строки %d равна %f\n",i,sum); #pragma omp atomic total = total + sum; } /* Завершение параллельного фрагмента */ printf ("Общая сумма элементов матрицы равна %f\n",total);7.8. Пример использования директивы atomic
Отметим, что в данном случае потоки работают непосредственно с общей переменной total.
Как можно видеть, директива atomic может быть применена только для простых выражений, но является наиболее эффективным средством организации взаимоисключения, поскольку многие из допустимых для директивы операций на самом деле выполняются как атомарные на аппаратном уровне. Тем не менее, следует отметить, что данный вариант программы в общем случае будет проигрывать варианту 7.6 по эффективности, поскольку теперь синхронизация потоков будет выполняться для каждой строки обрабатываемой матрицы, а в примере 7.6 количество моментов синхронизации ограничено числом потоков.
7.5.2. Использование критических секций
Действия над общими переменными могут быть организованы в виде критической секции, т.е. как блок программного кода, который может выполняться только одним потоком в каждый конкретный момент времени. При попытке входа в критическую секцию, которая уже исполняется одним из потоков используется, все другие потоки приостанавливаются ( блокируются ). Как только критическая секция освобождается, один из приостановленных потоков (если они имеются) активизируется для выполнения критической секции.
Определение критической секции в OpenMP осуществляется при помощи директивы critical, формат записи которой имеет вид:
#pragma omp critical [(name)] <block>
Как можно заметить, критические секции могут быть именованными - можно рекомендовать активное использование данной возможности для разделения критических секций, т.к. это позволит уменьшить число блокировок процессов.
Покажем использование механизма критических секций на примере нахождения максимальной суммы элементов строк матрицы. Одна из возможных реализаций состоит в следующем:
smax = -DBL_MAX; #pragma omp parallel for shared(a) private(i,j,sum) { for (i=0; i < NMAX; i++) { sum = 0; for (j=i; j < NMAX; j++) sum += a[i][j]; printf ("Сумма элементов строки %d равна %f\n",i,sum); if ( sum > smax ) #pragma omp criticalif ( sum > smax ) smax = sum; } /* Завершение параллельного фрагмента */ printf ("Максимальная сумма элементов строк матрицы равна %f\n",smax);7.9. Пример использования критических секций
Следует обратить внимание на реализацию проверки суммы элементов строки на максимум. Директиву critical можно записать до первого оператора if, однако это приведет к тому, что критическая секция будет задействована для каждой строки и это приведет к дополнительным блокировкам потоков. Лучший вариант - организовать критическую секцию только тогда, когда необходимо осуществить запись в общую переменную smax (т.е. когда сумма элементов строки превышает значение максимума). Отметим особо, что после входа в критическую секцию необходимо повторно проверить переменную sum на максимум, поскольку после первого оператора if и до входа в критическую секцию значение smax может быть изменено другими потоками. Отсутствие второго оператора if приведет к появлению трудно-выявляемой ошибки, учет подобных моментов представляет определенную трудность параллельного программирования.
7.5.3. Применение переменных семафорного типа (замков)
В OpenMP поддерживается специальный тип данных omp_lock_t, который близок к классическому понятию семафоров. Для переменных этого типа определены функции библиотеки OpenMP:
- Инициализировать замок:
void omp_init_lock(omp_lock_t *lock);
- Установить замок:
void omp_set_lock (omp_lock_t &lock);
Если при установке замок был установлен ранее, то поток блокируется.
- Освободить замок:
void omp_unset_lock (omp_lock_t &lock);
После освобождения замка при наличие блокированных на этом замке потоков один из них активизируется и замок снова отмечается как закрытый.
- Установить замок без блокировки:
int omp_test_lock (omp_lock_t &lock);
Если замок свободен, функция его закрывает и возвращает значение true. Если замок занят, поток не блокируется, и функция возвращает значение false.
- Перевод замка в неинициализированное состояние:
void omp_destroy_lock(omp_lock_t &lock)
Переработаем пример 7.8 так, чтобы для организации взаимного исключения при доступе к общим данным использовался механизм замков:
omp_lock_t lock; omp_init_lock(&lock); smax = -DBL_MAX; #pragma omp parallel for shared(a) private(i,j,sum) { for (i=0; i < NMAX; i++) { sum = 0; for (j=i; j < NMAX; j++) sum += a[i][j]; printf ("Сумма элементов строки %d равна %f\n",i,sum); if ( sum > smax ) { omp_set_lock (&lock);if ( sum > smax ) smax = sum; omp_unset_lock (&lock); } } /* Завершение параллельного фрагмента */ printf ("Максимальная сумма элементов строк матрицы равна %f\n",smax); omp_destroy_lock (&lock);7.10. Пример организации взаимоисключения при помощи замков
В OpenMP поддерживаются также и вложенные замки, которые предназначены для использования в ситуациях, когда внутри критических секций осуществляется вызов одних и тех же замков. Механизм работы с вложенными замками является тем же самым (в наименование функций добавляется поле nest ), т.е. их использование обеспечивается при помощи функций:
void omp_init_nest_lock(omp_nest_lock_t *lock); void omp_set_nest_lock (omp_nest_lock_t &lock); void omp_unset_nest_lock (omp_nest_lock_t &lock); int omp_test_nest_lock (omp_nest_lock_t &lock); void omp_destroy_nest_lock(omp_nest_lock_t &lock)
Как можно заметить, для описания вложенных замков используется тип omp_nest_lock_t.