При использовании многопроцессорных вычислительных систем с общей памятью обычно предполагается, что имеющиеся в составе системы процессоры обладают равной производительностью, являются равноправными при доступе к общей памяти, и время доступа к памяти является одинаковым (при одновременном доступе нескольких процессоров к одному и тому же элементу памяти очередность и синхронизация доступа обеспечивается на аппаратном уровне).
Перечисленному выше набору предположений удовлетворяют также активно развиваемые в последнее время многоядерные процессоры, в которых каждое ядро представляет практически независимо функционирующее вычислительное устройство. Для общности излагаемого учебного материала для упоминания одновременно и мультипроцессоров и многоядерных процессоров для обозначения одного вычислительного устройства (одноядерного процессора или одного
(рис 7.1) Архитектура многопроцессорных систем с общей (разделяемой) с однородным доступом памятью (для примера каждый процессор имеет два вычислительных ядра)Следует отметить, что общий доступ к данным может быть обеспечен и при физически распределенной памяти (при этом, естественно, длительность доступа уже не будет одинаковой для всех элементов памяти). Такой подход именуется как неоднородный доступ к памяти ( non-uniform memory access or NUMA ).
В самом общем виде системы с общей памятью могут быть представлены в виде модели
Обычный подход при организации вычислений для многопроцессорных вычислительных систем с общей памятью - создание новых параллельных методов на основе обычных последовательных программ, в которых или автоматически компилятором, или непосредственно программистом выделяются участки независимых друг от друга вычислений. Возможности автоматического анализа программ для порождения параллельных вычислений достаточно ограничены, и второй подход является преобладающим. При этом для разработки параллельных программ могут применяться как новые алгоритмические языки, ориентированные на параллельное программирование, так и уже имеющиеся языки программирования, расширенные некоторым набором операторов для параллельных вычислений.
Широко используемый подход состоит и в применении тех или иных библиотек, обеспечивающих определенный программный интерфейс ( application programming interface, API ) для разработки параллельных программ. В рамках такого подхода наиболее известны Windows Thread API (см., например, Вильямс (2001)) и PThead API (см., например, Butenhof (1997)). Однако первый способ применим только для ОС семейства Microsoft Windows, а второй вариант API является достаточно трудоемким для использования и имеет низкоуровневый характер.
Все перечисленные выше подходы приводят к необходимости существенной переработки существующего программного обеспечения, и это в значительной степени затрудняет широкое распространение параллельных вычислений. Как результат, в последнее время активно развивается еще один подход к разработке параллельных программ, когда указания программиста по организации параллельных вычислений добавляются в программу при помощи тех или иных внеязыковых средств языка программирования - например, в виде директив или комментариев, которые обрабатываются специальным препроцессором до начала компиляции программы. При этом исходный текст программы остается неизменным, и по нему, в случае отсутствия препроцессора, компилятор построит исходный последовательный программный код. Препроцессор же, будучи примененным, заменяет директивы параллелизма на некоторый дополнительный программный код (как правило, в виде обращений к процедурам какой-либо параллельной библиотеки).
Рассмотренный выше подход является основой технологии OpenMP (см., например, Chandra et al. (2000)), наиболее широко применяемой в настоящее время для организации параллельных вычислений на многопроцессорных системах с общей памятью. В рамках данной технологии директивы параллелизма используются для выделения в программе параллельных фрагментов, в которых последовательный исполняемый код может быть разделен на несколько раздельных командных потоков ( threads ). Далее эти потоки могут исполняться на разных процессорах (
(рис 7.2) Общая схема выполнения параллельной программы при использовании технологии OpenMPПри разработке технологии
Далее в разделе будет приведено последовательное описание возможностей технологии
Перед началом практического изучения технологии
Под параллельной программой в рамках
Важно отметить, что разделение вычислений между потоками осуществляется под управлением соответствующих директив
Потоки могут выполняться на разных процессорах (
Количество потоков определяется в начале выполнения параллельных фрагментов программы и обычно совпадает с количеством имеющихся np-1, где np есть общее количество потоков. Номер потока также может быть получен при помощи функции
Использование в технологии
Как уже отмечалось ранее, потоки исполняются в
n=n+1;
для общей переменной n. Тогда в зависимости от условий выполнения данная операция может быть выполнена поочередно (что приведет к получению правильного результата) или же оба потока могут одновременно прочитать значение переменной n, одновременно увеличить и записать в эту переменную новое значение (как результат, будет получено неправильное значение). Подобная ситуация, когда результат вычислений зависит от темпа выполнения потоков, получил наименование гонки потоков ( ). Для исключения гонки необходимо обеспечить, чтобы изменение значений общих переменных осуществлялось в каждый момент времени только одним единственным потоком - иными словами необходимо обеспечить взаимное исключение ( ) потоков при работе с общими данными. В ) операций, механизма кри
тических секций ( ) или специального типа семафоров - замков ( locks ).
Следует отметить, что организация взаимного исключения приводит к уменьшению возможности параллельного выполнения потоков - при одновременном доступе к общим переменным только один из них может продолжить работу, все остальные потоки будут блокированы и будут ожидать освобождения общих данных. Можно сказать, что именно при реализации взаимодействия потоков проявляется искусство параллельного программирования для вычислительных систем с общей памятью - организация взаимного исключения при работе с общими данными является обязательной, но возникающие при этом задержки (блокировки) потоков должны быть минимальными по времени.
Помимо взаимоисключения, при параллельном выполнении программы во многих случаях является необходимым та или иная синхронизация ( synchronization ) вычислений, выполняемых в разных потоках: например, обработка данных, выполняемая в одном потоке, может быть начата только после того, как эти данные будут сформированы в другом потоке (классическая задача параллельного программирования " производитель-потребитель " - "producer- ). В .
Конструктивно в составе технологии
Именно в таком порядке и будут рассмотрены возможности технологии
Стандарт предусматривает использование
В самом общем виде формат директив
#pragma omp <имя_директивы> [<параметр>[[,] <параметр>]…]
Начальная часть директивы (#clause ).
Для иллюстрации приведем пример директивы:
#pragma omp parallel default(shared) \
private(beta,pi)
Пример показывает также, что для задания директивы может быть использовано несколько строк программы - признаком наличия продолжения является знак обратного слеша "\".
Действие директивы распространяется, как правило, на следующий в программе оператор, который может быть, в том числе, и структурированным блоком.
Итак, параллельная программа, разработанная с использованием
Для выделения параллельных фрагментов программы следует использовать директиву parallel:
#pragma omp parallel [<параметр> ...] <блок_программы>
Для блока (как и для блоков всех других директив
Директива parallel является одной из основных директив
parallel, создается набор ( team ) из N потоков; исходный поток программы является основным потоком этого набора ( master thread ) и имеет номер 0.Подчеркнем чрезвычайно важный момент - оказывается, даже такого краткого рассмотрения возможностей технологии
#include <omp.h>
main () {
/* Выделение параллельного фрагмента*/
#pragma omp parallel
{
printf("Hello World !\n");
}/* Завершение параллельного фрагмента */
}
В приведенной программе файл omp.h содержит определения parallel будут созданы потоки (по умолчанию их количество совпадает с числом имеющихся
После рассмотрения примера важно отметить также, что параллельное выполнение программы будет осуществляться не только для программного блока, непосредственно следующего за директивой parallel, но и для всех функций, вызываемых из этого блока (см. рис. 7.3). Для обозначения этих динамически-возникающих параллельно выполняемых участков программного кода в parallel region ) - ранее, в предшествующих версиях стандарта использовался термин динамический контекст ( dynamic ).
(рис 7.3) Область видимости директив OpenMPРяд директив ).
Для более точного понимания излагаемого учебного материала сведем воедино введенные термины и понятия (в скобках даются названия, используемые в стандарте 2.5):
parallel construct ) - блок программы, управляемый директивой parallel ; именно параллельные фрагменты, совместно с параллельными областями, представляют параллельно-выполняемую часть программы; в предшествующих стандартах для обозначения данного понятия использовался термин лексический контекст ( lexical extent ) директивы parallel.parallel region ) - параллельно выполняемые участки программного кода, динамически-возникающие в результате вызова функций из параллельных фрагментов - см. рис. 7.3.parallel section ) - часть параллельного фрагмента, выделяемая для параллельного выполнения при помощи директивы section - см. подраздел 7.6.Приведем перечень параметров директивы parallel:
if (scalar_expression)private (list) shared (list) default (shared | none) firstprivate (list) reduction (operator: list) copyin (list)num_threads (scalar_expression)Список параметров приведен только для справки, пояснения по использованию этих параметров будут даны позднее по мере изложения учебного материала.
Практически сразу после разработки первой параллельной программы появляется необходимость определения времени выполнения вычислений, поскольку в большинстве случаев основной целью использования параллельных вычислительных систем является сокращений времени выполняемых расчетов. Используемые обычно средства для измерения времени работы программ зависят, как правило, от аппаратной платформы, операционной системы,
Получение текущего момента времени выполнения программы обеспечивается при помощи функции:
double omp_get_wtime(void),
результат вызова которой есть количество секунд, прошедших от некоторого определенного момента времени в прошлом. Этот момент времени в прошлом, от которого происходит отсчет секунд, может зависеть от среды реализации omp_get_wtime следует использовать только для определения длительности выполнения тех или иных фрагментов кода параллельных программ. Возможная схема применения функции omp_get_wtime может состоять в следующем:
double t1, t2, dt; t1 = omp_get_wtime (); … t2 = omp_get_wtime (); dt = t2 - t1;
Точность измерения времени также может зависеть от среды выполнения параллельной программы. Для определения текущего значения точности может быть использована функция:
double omp_get_wtick(void),
позволяющая определить время в секундах между двумя последовательными показателями времени аппаратного таймера используемой компьютерной системы.
Как уже отмечалось ранее, программный код блока директивы parallel по умолчанию исполняется всеми потоками. Данный способ может быть полезен, когда нужно выполнить одни и те же действия многократно (как в примере 7.1) или когда один и тот же программный код может быть применен для выполнения обработки разных данных. Последний вариант достаточно широко используется при разработке parallel - можно осуществить разделение итеративно-выполняемых действий в циклах для непосредственного указания, над какими данными должными выполняться соответствующие вычисления. Такая возможность является тем более важной, поскольку
во
многих случаях именно в циклах выполняется основная часть вычислительно-трудоемких вычислений.
Для распараллеливания циклов в for:
#pragma omp for [<параметр> ...] <цикл_for>
После этой директивы итерации цикла распределяются между потоками и, как результат, могут быть выполнены параллельно (см. рис. 7.4) - понятно, что такое распараллеливание возможно только, если между итерациями цикла нет информационной зависимости.
(рис 7.4) Общая схема распараллеливания цикловВажно отметить, что для распараллеливания цикл for должен иметь некоторый "канонический" тип цикла со
for (index = first; index < end; increment_expr)
Здесь index должен быть целой переменной; на месте знака " < "
в выражении для проверки окончания цикла может находиться любая операция сравнения
" <= ", " > " или " >= ".
Операция изменения переменной цикла должна иметь одну из следующих форм:
index++, ++index,index--, --index,index+=incr, index-=incr,index=index+incr, index=incr+index,index=index-incrИ, конечно же, переменные, используемые в заголовке
В качестве примера использования директивы рассмотрим учебную задачу вычисления суммы элементов для каждой строки прямоугольной матрицы:
#include <omp.h>
#define CHUNK 100
#define NMAX 1000
main () {
int i, j, sum;
float a[NMAX][NMAX];
<инициализация данных>
#pragma omp parallel shared(a) private(i,j,sum)
{
#pragma omp for
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=0; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
} /* Завершение параллельного фрагмента */
}
В приведенной программе для директивы parallel появились два параметра - их назначение будет описано в следующем подразделе, здесь же отметим, что параметры директивы shared и private определяют доступность данных в потоках программы - переменные, описанные как shared, являются общими для потоков; для переменных с описанием private создаются отдельные копии для каждого потока, эти локальные копии могут использоваться в потоках независимо друг от друга.
Следует отметить, что если в блоке директивы parallel нет ничего, кроме директивы for, то обе директивы можно объединить в одну, т.е. пример 7.2 может быть переписан в виде:
#include <omp.h>
#define NMAX 1000
main () {
int i, j, sum;
float a[NMAX][NMAX];
<инициализация данных>
#pragma omp parallel for shared(a) private(i,j,sum)
{
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=0; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
} /* Завершение параллельного фрагмента */
}
Параметрами директивы for являются:
schedule (type [,chunk ])orderednowait private (list) shared (list)firstprivate (list)lastprivate (list)reduction (operator: list)Последние пять параметров директивы будут рассмотрены в следующем подразделе, здесь же приведем описание оставшихся параметров.
При разном объеме вычислений в разных итерациях цикла желательно иметь возможность управлять распределением итераций цикла между потоками - в schedule директивы for. Поле type параметра schedule может принимать следующие значения:
static - статический способ распределения итераций до начала выполнения цикла. Если поле chunk не указано, то итерации делятся поровну между потоками. При заданном значении chunk итерации цикла делятся на блоки размера chunk и эти блоки распределяются между потоками до начала выполнения цикла.dynamic - динамический способ распределения итераций. До начала выполнения цикла потокам выделяются блоки итераций размера chunk (если поле chunk не указано, то полагается значение chunk =1 ). Дальнейшее выделение итераций (также блоками размера chunk ) осуществляется в момент завершения потоками своих ранее назначенных итераций.guided - управляемый способ распределения итераций. Данный способ близок к предшествующему варианту, отличие состоит только в том, что начальный размер блоков итераций определяется в соответствии с некоторым параметром среды реализации chunk есть некоторая доля предшествующего значения) при каждом новом выделении блока итераций. При этом получаемый размер блока итераций не должен быть меньше значения chunk (если поле chunk не указано, то полагается значение chunk =1 ).runtime - способ распределения OMP_SCHEDULE. Так, например, для задания динамического способа при размере блока итераций 3, следует определить:setenv OMP_SCHEDULE "dynamic,3"
Полезность такого варианта очевидна - способ распределения итераций между потоками можно менять, не корректируя при этом код программы (т.е. без повторной компиляции и сборки программы).
Для демонстрации примера использования параметра schedule предположим, что матрица в примере 7.3 имеет верхний треугольный вид - в этом случае объем вычислений для каждой строки является различным и последовательное распределение итераций поровну приведет к неравномерному распределению вычислительной нагрузки между потоками. Для балансировки расчетов можно применить статическую или динамическую схемы распределения итераций:
#include <omp.h>
#define CHUNK 100
#define NMAX 1000
main () {
int i, j, sum;
float a[NMAX][NMAX];
<инициализация данных>
#pragma omp parallel for shared(a) private(i,j,sum) \
schedule (dynamic, CHUNK)
{
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=i; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
} /* Завершение параллельного фрагмента */
}
В результате распараллеливания цикла порядок выполнения итераций не фиксирован: в зависимости от состояния среды выполнения очередность выполнения итераций может меняться. Если же для ряда действий в цикле необходимо сохранить первичный порядок вычислений, который соответствует последовательному выполнению итераций в последовательной программе, то желаемого результата можно добиться при помощи директивы ordered (при этом для директивы for должен быть указан параметр ordered ). Поясним сказанное на примере нашей учебной задачи. Для приведенного выше варианта программы печать сумм элементов строк матрицы будет происходить в некотором произвольном порядке; при необходимости печати по порядку расположения строк следует провести следующее изменение программного кода:
#pragma omp parallel for shared(a) private(i,j,sum) \
schedule (dynamic, CHUNK) ordered {
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=i; j < NMAX; j++)
sum += a[i][j];
#pragma omp ordered
printf ("Сумма элементов строки %d равна %f\n",i,sum);
} /* Завершение параллельного фрагмента */
Поясним дополнительно, что параметр ordered управляет порядком выполнения только тех действий, которые выделены директивой ordered - выполнение оставшихся действий в итерациях цикла по-прежнему может происходить параллельно. Важно помнить также, что директива ordered может быть применена в теле цикла только один раз.
Следует отметить, что указание необходимости сохранения порядка вычислений может привести к задержкам при
По умолчанию, все потоки, прежде чем перейти к выполнению дальнейших вычислений, ожидают окончания выполнения итераций цикла даже если некоторые из них уже завершили свои вычисления - конец цикла представляет собой некоторый барьер, который потоки могут преодолеть только все вместе. Можно отменить указанную синхронизацию, указав параметр в директиве for - тогда потоки могут продолжить вычисления за переделами цикла, если для них нет итераций цикла для выполнения.
Теперь при наличии учебного примера можно пояснить назначение параметра if директивы parallel.
При разработке if директивы parallel, задавая при его помощи условие создания параллельного фрагмента (если условие параметра if не выполняется, блок директивы parallel выполняется как обычный последовательный код). Так, например, в нашем учебном примере можно ввести условие, определяющее минимальный размер матрицы, при котором осуществляется
#include <omp.h>
#define NMAX 1000
#define LIMIT 100
main () {
int i, j, sum;
float a[NMAX][NMAX];
<инициализация данных>
#pragma omp parallel for shared(a) private(i,j,sum) if (NMAX>LIMIT)
{
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=0; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
} /* Завершение параллельного фрагмента */
}
В приведенном примере параллельный фрагмент создается только, если порядок матрицы превышает 100 (т.е. когда обеспечивается некоторый минимальный объем выполняемых вычислений).
Как уже отмечалось ранее, потоки параллельной программы выполняются в
Параметры shared и private директивы for для управления доступа к переменным уже использовались в примере 7.2. Параметр shared определяет переменные, которые будут общими для всех потоков. Параметр private указывает переменные, для которых в каждом потоке будут созданы локальные копии - они будут доступны только внутри каждого потока в отдельности (значения локальных переменных потока недоступны для других потоков). Параметры shared и private могут повторяться в одной и той же директиве несколько раз, имена переменных должны быть уже ранее определены и не могут повторяться в списках параметров shared и private.
По умолчанию все переменные программы являются общими. Такое соглашение приводит к тому, что компилятор не может указать на ошибочные ситуации, когда программисты забывают описывать локальные переменные потоков в параметре private (отсутствие таких описаний приводит к тому, что переменные будут восприниматься как глобальные). Для выявления таких ошибок можно воспользоваться параметром default директивы parallel для изменения правила по умолчанию:
default ( shared | none )
Как можно видеть, при помощи этого параметра можно отменить действие правила по умолчанию ( default(none) ) или восстановить правило, что по умолчанию переменные программы являются общими ( default(shared) ).
Следует отметить, что начальные значения локальных переменных не определены, а конечные значения теряются при завершении потоков. Для инициализации можно использовать параметр firstprivate директивы for, по которому начальные значения локальных переменных будут устанавливаться в значения, которые существовали в переменных до момента создания локальных копий. Запоминание конечных значений обеспечивается при помощи параметра lastprivate, в соответствии с которым значения локальных переменных копируются из потока, выполнившего последнюю итерацию. Поясним сказанное на примере рис. 7.5. На рисунке показана переменная sum, которая определена как lastprivate в директиве parallel for. Для этой переменной создаются локальные копии в каждом потоке, при завершении параллельного участка программного кода значение локальной переменной потока, выполнившего последнюю итерацию цикла, переписывается в исходную п
еременную sum.
(рис 7.5) Общая схема работы с локальными переменными потоков
Использование параметра lastprivate позволяет сохранить значений локальной переменной одного из потоков, но во многих случаях для обработки могут понадобиться значения всех локальных переменных. Данная возможность может быть обеспечена, например, сохранением этих значений в общих переменных - более подробно правила работы с общими переменными будет рассмотрена в следующем подразделе. Другой подход состоит в использовании коллективных операций над локальными переменными, предусмотренными в директивы for:
reduction (operator: list)
где список list задает набор локальных переменных (повторное описание в параметре private переменных из списка list не требуется), для которых должна быть выполнена коллективная операция, а поле operator указывает тип этой коллективной операции. Допустимыми значениями для поля operator являются следующие операции (которые не могут быть перегружены):
+, -, *, , |, ^, , ||
Операция
x = x <operator> <expression>x = <expression> <operator> x (за исключением операции вычитания)x <op >= <expression>x++, ++x, x--, --xгде x есть имя скалярной переменной, выражение expression не должно включать переменную x, возможные операции для поля operator совпадают с выше приведенным списком, а допустимыми значениями для поля являются операции:
+, -, *, , |, ^
В качестве примера можно добавить в нашу учебную задачу действие по сложению всех сумм элементов строк матрицы - возможный программный код может быть следующим:
total = 0;
#pragma omp parallel for shared(a) private(i,j,sum) reduction (+:total)
{
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=i; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
total = total + sum;
} /* Завершение параллельного фрагмента */
printf ("Общая сумма элементов матрицы равна %f\n",total);
В качестве пояснений добавим, что по параметру для переменной total создаются локальные переменные, затем полученные значения в потоках суммируются и запоминаются в исходной переменной. Подчеркнем, использование общей переменной total без создания локальных копий для накопления общей суммы при использовании в потоках операции
total = total + sum;
является неправильным, поскольку без обеспечения взаимоисключения при использовании общей переменной возникает ситуация гонки потоков и итоговый результат может быть ошибочным (см. следующий подраздел).
Как уже неоднократно отмечалось ранее, при изменении общих данных несколькими потоками должны быть обеспечены условия взаимоисключения - изменение значений общих переменных должно осуществляться в каждый конкретный момент времени только одним потоком. Рассмотрим возможные способы организации взаимоисключения.
Действие над общей переменной может быть выполнено как атомарная (неделимая) операция при помощи директивы . Формат директивы имеет вид:
#pragma omp atomic <expression>
где expression должно иметь вид:
x++ ; или ++x ; или x-- ; или --x;
где x есть имя любой целой скалярной переменной.
Директива может быть записана и в виде:
#pragma omp atomic x <operator>= <expression>
где x есть имя скалярной переменной, выражение expression не должно включать переменную x, а допустимыми значениями для поля operator являются следующие операции (которые не могут быть перегружены):
+, *, -, /, , |, ^, >>, <<
Как следует из названия, операция директивы выполняется как неделимое действие над указанной общей переменной, и, как результат, никакие другие потоки не могут получить доступ к этой переменной в этот момент времени.
Применим рассмотренную директиву для нашей учебной задачи при вычислении общей суммы:
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);
Отметим, что в данном случае total.
Как можно видеть, директива может быть применена только для простых выражений, но является наиболее эффективным средством организации взаимоисключения, поскольку многие из допустимых для директивы операций на самом деле выполняются как атомарные на аппаратном уровне. Тем не менее, следует отметить, что данный вариант программы в общем случае будет проигрывать варианту 7.6 по эффективности, поскольку теперь
Действия над общими переменными могут быть организованы в виде критической секции, т.е. как блок программного кода, который может выполняться только одним потоком в каждый конкретный момент времени. При попытке входа в критическую секцию, которая уже исполняется одним из потоков используется, все другие потоки приостанавливаются ( блокируются ). Как только критическая секция освобождается, один из приостановленных потоков (если они имеются) активизируется для выполнения
Определение , формат записи которой имеет вид:
#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);
Следует обратить внимание на реализацию проверки суммы элементов строки на максимум. Директиву можно записать до первого оператора if, однако это приведет к тому, что критическая секция будет задействована для каждой строки и это приведет к дополнительным блокировкам потоков. Лучший вариант - организовать критическую секцию только тогда, когда необходимо осуществить запись в общую переменную smax (т.е. когда сумма элементов строки превышает значение максимума). Отметим особо, что после входа в критическую секцию необходимо повторно проверить переменную sum на максимум, поскольку после первого оператора if и до входа в критическую секцию значение smax может быть изменено другими потоками. Отсутствие второго оператора if приведет к появлению трудно-выявляемой ошибки, учет подобных моментов представляет определенную трудность параллельного программирования.
В omp_lock_t, который близок к классическому понятию семафоров. Для переменных этого типа определены функции библиотеки
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);
В ), т.е. их использование обеспечивается при помощи функций:
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.
Напомним, что в рассмотренном ранее учебном материале для построения параллельных программ был изложен подход (см. подразделы 7.2 - 7.3), обычно именуемый распараллеливанием по данным, в рамках которого обеспечивается одновременное (параллельное) выполнение одних и тех же вычислительных действий над разными наборами обрабатываемых данных (например, как в нашем учебном примере, суммирование элементов разных строк матрицы). Другая также широко встречающаяся ситуация состоит в том, что для решения поставленной задачи необходимо выполнить разные процедуры обработки данных, при этом данные процедуры или полностью не зависят друг от друга, или же являются слабо связанными. В этом случае такие процедуры можно выполнить параллельно; такой подход обычно именуется распараллеливанием по задачам. Для поддержки такого способа организации п
араллельных вычислений в parallel, можно выделять параллельно выполняемые программные секции (директива sections ).
Формат директивы sections имеет вид:
#pragma omp sections [<параметр> ...]
{
#pragma omp section
<блок_программы>
#pragma omp section
<блок_программы>
}
При помощи директивы sections выделяется программный код, который далее будет разделен на параллельно выполняемые секции. Директивы section определяют секции, которые могут быть выполнены параллельно (для первой по порядку секции директива section не является обязательной) - см. рис. 7.6. В зависимости от взаимного сочетания количества потоков и количества определяемых секций, каждый поток может выполнить одну или несколько секций (вместе с тем, при малом количестве секций некоторые потоки могут оказаться и без секций и окажутся незагруженными). Как результат, можно отметить, что использование секций достаточно сложно поддается масштабированию (настройке на число имеющихся потоков). Кроме того, важно помнить, что в соответствии со стандартом, порядок выполнения программных секций не определен, т.е. секции могут быть выполнены потоками в произвольном порядке.
(рис 7.6) Общая схема выполнения параллельных секций директивы sectionsВ качестве примера использования директивы расширим нашу учебную задачу действиями по копированию обрабатываемой матрицы (в качестве исходного варианта используется пример 7.6):
total = 0;
#pragma omp parallel shared(a,b) private(i,j)
{
#pragma omp sections /* Вычисление сумм элементов строк и общей суммы */
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=i; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
total = total + sum;
}
#pragma omp section
/* Копирование матрицы */
for (i=0; i < NMAX; i++) {
for (j=i; j < NMAX; j++)
b[i][j] = a[i][j];
}
} /* Завершение параллельного фрагмента */
printf ("Общая сумма элементов матрицы равна %f\n",total);
Для обсуждения примера можно отметить, что выполняемые в программе вычисления (суммирование и копирование элементов) можно было бы объединить в рамках одних и тех же циклов, однако приведенное разделение тоже может оказаться полезным, поскольку в результате разделения данные вычисления могут быть легко векторизованы (конвейеризированы) компилятором. Важно отметить также, что сформированные подобным образом программные секции не сбалансированы по вычислительной нагрузке (первая секция содержит больший объем вычислений).
В качестве параметров директивы sections могут использоваться:
private (list) firstprivate (list) lastprivate (list) reduction (operator: list) nowait Все перечисленные параметры уже были рассмотрены ранее. Отметим, что по умолчанию выполнение директивы sections синхронизировано, т.е. потоки, завершившие свои вычисления, ожидают окончания работы всех потоков для одновременного завершения директивы.
В заключение можно добавить, что также как и для директивы for, в случае если в блоке директивы parallel присутствует только директива sections, то данные директивы могут быть объединены. С использованием данной возможности пример 7.11 может быть переработан к виду:
total = 0;
#pragma omp parallel sections shared(a,b) private(i,j)
{
/* Вычисление сумм элементов строк и общей суммы */
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=i; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
total = total + sum;
}
#pragma omp section
/* Копирование матрицы */
for (i=0; i < NMAX; i++) {
for (j=i; j < NMAX; j++)
b[i][j] = a[i][j];
}
} /* Завершение параллельного фрагмента */
printf ("Общая сумма элементов матрицы равна %f\n",total);
При выполнении параллельных фрагментов может оказаться необходимым реализовать часть программного кода только одним потоком (например, открытие файла). Данную возможность в single и master.
Формат директивы single имеет вид:
#pragma omp single [<параметр> ...] <блок_программы>
Директива single определяет блок параллельного фрагмента, который должен быть выполнен только одним потоком; все остальные потоки ожидают завершения выполнения данного блока (если не указан параметр ) - см. рис. 7.7.
(рис 7.7) Общая схема выполнения директивы singleВ качестве параметров директивы могут использоваться:
private (list)firstprivate (list) copyprivate (list)nowait Новым в приведенном списке является только параметр copyprivate, который обеспечивает копирование переменных, указанных в списке list, после выполнения блока директивы single в локальные переменные всех остальных потоков.
Формат директивы master имеет вид:
#pragma omp master <блок_программы>
Директива master определяет фрагмент кода, который должен быть выполнен только основным потоком; все остальные потоки пропускают данный фрагмент
При помощи директивы можно определить точку синхронизации, которую должны достигнуть все потоки для продолжения вычислений (директива может находиться в пределах как параллельного фрагмента так и параллельной области, т.е. директива является отделяемой).
Формат директивы имеет вид:
#pragma omp barrier
Директива позволяетопределить точку синхронизации, в которой системой должно быть обеспечено единое для всех потоков состояние памяти (т.е. если потоком какое-либо значение извлекалось из памяти для модификации, измененное значение обязательно должно быть записано в
Формат директивы имеет вид:
#pragma omp flush [(list)]
Как показывает формат, директива содержит список list с перечнем переменных, для которых выполняется синхронизация; при отсутствии списка синхронизация выполняется для всех переменных потока.
Следует отметить, что директива неявным образом присутствует в директивах .
Как описывалось при рассмотрении директивы parallel, для потоков могут быть определены локальные переменные (при помощи параметров private, firstprivate, lastprivate ), которые создаются в начале соответствующего параллельного фрагмента и удаляются при завершении потоков. В threadprivate.
Формат директивы threadprivate имеет вид:
#pragma omp threadprivate (list)
Список list определяет набор определяемых переменных. Созданные локальные копии не видимы в последовательных участках выполнения программы (т.е. вне параллельных фрагментов), но существуют в течение всего времени выполнения программы. Указываемые в списке переменные должны быть уже определены в программе; объявление переменных в директиве должно предшествовать использованию переменных в потоках.
Следует отметить, что использование директивы threadprivate позволяет решить еще одну проблему. Дело в том, что действие параметров private распространяется только на программный код параллельных фрагментов, но не параллельных областей - т.е., например, любая локальная переменная, определенная в параллельном фрагменте функции root на рис. 7.3, будет недоступна в параллельной области функции node. Выход из такой ситуации может быть или в передаче значений локальных переменных через параметры функций или же в использовании постоянных локальных переменных директивы threadprivate.
Отметим еще раз, что полученные в потоках значения постоянных переменных сохраняются между параллельными фрагментами программы. Значения этих переменных можно переустановить при начале параллельного фрагмента по значениям из основного потока при помощи параметра copyin директивы parallel.
Формат параметра copyin директивы parallel имеет вид:
copyin (list)
Для демонстрации рассмотренных понятий приведем пример A.32.1c из стандарта
#include <stdlib.h>
float* work;
int size;
float tol;
#pragma omp threadprivate(work,size,tol)
void a32( float t, int n )
{
tol = t;
size = n;
#pragma omp parallel copyin(tol,size)
{
build();
}
}
void build()
{
int i;
work = (float*)malloc( sizeof(float)*size );
for( i = 0; i < size; ++i ) work[i] = tol;
}
В приведенном примере определяются постоянно существующие локальные переменные work, size, tol для потоков (директива threadprivate ). Перед началом параллельного фрагмента значения этих переменных из основного потока копируются во все потоки (параметр copyin ). Значения постоянно существующих локальных переменных доступны в параллельной области функции build (для обычных локальных переменных потоков их значения пришлось бы передавать через параметры функции).
По умолчанию количество создаваемых потоков определяется реализацией и обычно совпадает с числом имеющихся
Прежде всего, количество создаваемых потоков может быть задано при помощи параметра num_threads директивы parallel.
Количество необходимых потоков может быть также задано при помощи функции omp_set_num_threads библиотеки
Формат функции omp_set_num_threads имеет вид:
void omp_set_num_threads (int num_threads);
Вызов функции omp_set_num_threads должен осуществляться из последовательной части программы.
Для задания необходимого количества потоков можно воспользоваться и переменной окружения OMP_NUM_THREADS. При использовании нескольких способов задания наибольший приоритет имеет параметр num_threads директивы parallel, затем функция библиотеки, затем переменная окружения.
Изменение количества создаваемых потоков может быть полезно и для целей отладки разрабатываемой параллельной программы для проверки ее работоспособности при разном количестве потоков.
Приведем здесь также дополнительные функции библиотеки
int omp_get_max_threads(void)
int omp_get_num_threads(void)
int omp_get_thread_num(void)
int omp_get_num_procs(void)
Количество создаваемых потоков в параллельных фрагментах программы по умолчанию является фиксированным (см. также предыдущий пункт), однако стандартом предусматривается возможность динамического режима, когда количество потоков может определяться реализацией для оптимизации функционирования вычислительной системы. Разрешение динамического режима и его отключение осуществляется при помощи функции omp_set_dynamic библиотеки
Формат функции omp_set_ dynamic имеет вид:
void omp_set_dynamic (int dynamic);
Вызов функции omp_set_dynamic должен осуществляться из последовательной части программы. Функция разрешает ( dynamic=true )илиотключает ( dynamic=false )динамический режим создания потоков.
Для управления динамическим режимом создания потоков можно воспользоваться и переменной окружения OMP_DYNAMIC. При использовании нескольких способов задания наибольший приоритет имеет функция библиотеки, затем переменная окружения.
Для получения состояния динамического режима можно воспользоваться функций omp_get_dynamic библиотеки
int omp_get_dynamic (void);
Параллельные фрагменты программы могут быть вложенными - такая возможность определяется реализацией
Управление режимом выполнения вложенных фрагментов осуществляется при помощи функции omp_set_nested библиотеки
Формат функции omp_set_nested имеет вид:
void omp_set_nested (int nested);
Вызов функции omp_set_nested должен осуществляться из последовательной части программы. Функция разрешает (
Для управления режимом поддержки вложенных параллельных фрагментов можно воспользоваться и переменной окружения OMP_NESTED. При использовании нескольких способов задания наибольший приоритет имеет функция библиотеки, затем переменная окружения.
Для получения состояния режима поддержки вложенных параллельных фрагментов можно воспользоваться функций omp_get_nested библиотеки
int omp_get_nested (void);
Итак, возможности технологии
Между разработкой параллельных программ с использованием
Прежде всего формат директив
<маркер> <имя_директивы> [<параметр>…]
Вид маркера зависит от формата записи программы:
!$omp или c$omp или *$omp
Маркер должен начинаться с позиции 1 строки. При этом сама директива может быть записана в несколько строк; первая строка директивы должна содержать в позиции 6 или пробел или 0; строки продолжения директивы в позиции 6 должны быть отмечены любым произвольным символом, отличающимся от пробела и 0.
!$omp. Маркер может быть записан, начиная с произвольной позиции строки, при этом маркеру должны предшествовать только пробелы или знаки табуляции. При размещении директивы на нескольких строках первая строка должна заканчиваться символом , строки продолжения могут начинаться с этого знака, но приводить его в строках продолжения не обязательно.В качестве основных отличий параллельных программ с использованием
end critical end do end master end ordered end parallel end sections end single end workshare
workshare, которая является близкой по своему назначению директиве sections.CALL.В качестве принятых соглашений при разработке программ на языке Fortran рекомендуется записывать имена подпрограмм с использованием прописных символов.
В качестве примера приведем вариант программы из примера 7.6 на
PROGRAM Example
INCLUDE "omp_lib.h"
INTEGER NMAX 1000
INTEGER LIMIT 100
INTEGER I, J, SUM
REAL A(NMAX,NMAX)
<инициализация данных>
!$OMP PARALLEL DO SHARED(A) PRIVATE(I,J,SUM) IF (NMAX>LIMIT)
DO I = 1, NMAX
SUM = 0
DO J = 1, NMAX
SUM = SUM + A(I,J)
ENDDO
PRINT * "Сумма элементов строки ", I, "равна ", SUM
ENDDO
!$OMP END PARALLEL DO ! Завершение параллельного фрагмента
STOP
END
При разработке параллельной программы с использованием
Поддержка единственности программного кода относится к числу важных положительных качеств технологии
Однако проблемы поддержки единственности программного кода возникают при использовании в параллельной программе функций и переменных окружения _OPENMP, значением которой является дата поддерживаемого стандарта в формате ГГГГММ (год, месяц). Как результат, для обеспечения единственности программного кода последовательного и параллельного вариантов программ, все
#ifdef _OPENMP <OpenMP-зависимый программный код> #endif
Последовательный вариант программы может порождаться и компиляторами с поддержкой
Среди компиляторов с поддержкой
| Параметр | Описание | |
|---|---|---|
| Windows | Linux | |
| -Qopenmp | - |
Компиляция программного кода с использованием |
| -Qopenmp-profile | - |
Компиляция с инструментацией программного кода с использованием |
| -Qopenmp- |
- |
Компиляция программного кода как обычной последовательной программы (с игнорированием директив и использованием функций-заглушек для функций |
Следует также помнить, что при сборке программы следует использовать библиотеки, обеспечивающие поддержки многопоточности.
Среди других компиляторов можно отметить:
Полный перечень компиляторов, поддерживающих
Данный раздел посвящен рассмотрению методов параллельного программирования для вычислительных систем с общей памятью с использованием технологии
В самом начале раздела отмечается, что технология threads ). Далее эти потоки могут исполняться на разных процессорах (
В подразделе 7.1 рассматривается ряд понятий и определений, являющихся основополагающими для стандарта
В подразделе 7.2 проводится быстрое и простое введение в разработку параллельных программ с использованием parallel и приводится пример первой параллельной программы с использованием
В подразделе 7.3 рассматриваются вопросы распределения вычислительной нагрузки между потоками на примере распараллеливания циклов по данным. Дается описание директивы for и описываются способы управления распределением итераций цикла между потоками.
В подразделе 7.4 подробно обсуждаются вопросы управления данными для параллельно выполняемых потоков. Дается понятие общих и локальных переменных для потоков. Приводится описание важной и часто встречающейся при обработке общих данных операции редукции.
В подразделе 7.5 рассматриваются вопросы организации взаимоисключения при использовании общих
В подразделе 7.6 излагаются вопросы распределения вычислительной нагрузки между потоками на основе распараллеливания задач. Дается описание директивы sections, приводятся примеры использования данной директивы.
В подразделе 7.7 рассматриваются расширенные возможности технологии master, single, , , threadprivate, copyin ), обсуждаются возможности управления потоками (количество создаваемых потоков, динамический режим создания потоков, вложенность параллельных фрагментов).
В завершающем подразделе 7.8 даются дополнительные сведения о технологии
В наиболее полном виде информация по параллельному программированию для вычислительных систем с общей памятью с использованием
Достаточно много информации о технологии
Для рассмотрения общих вопросов параллельного программирования для вычислительных систем с общей памятью может быть рекомендована работа Andrews (2000).
parallel?sections )?single и master )?flush )?threadprivate и copyin )?(описание методов интегрирования дано, например, в Kahaner, Moler and Nash (1988)).
,
reduction директивы for.sections.Для справки приведем перечень директив
| Директива | Описание |
|---|---|
parallel [<параметр>…] |
Директива определения параллельного фрагмента в коде программы (подраздел 7.2). Параметры: if, private, shared, default, firstprivate, |
for [<параметр>…] |
Директива распараллеливания циклов (подраздел 7.2). Параметры: private, firstprivate, lastprivate, |
sections [<параметр>…] |
Директива для выделения программного кода, который далее будет разделен на параллельно выполняемые параллельные секции (подраздел 7.6). Выделение параллельных секций осуществляется при помощи директивы section. Параметры: private, firstprivate, lastprivate, |
section |
Директива выделения параллельных секций - должна располагаться в блоке директивы sections (подраздел 7.6). |
single [<параметр>…] |
Директива для выделения программного кода в параллельном фрагменте, исполняемого только одним потоком (п. 7.7.1). Параметры: private, firstprivate, copyprivate, |
parallel for
[< параметр >… |
Объединенная форма директив parallel и for (подраздел 7.2). Параметры: private, firstprivate, lastprivate, shared, default, |
parallel sections [<параметр>… |
Объединенная форма директив parallel и sections (подраздел 7.6). Параметры: private, firstprivate, lastprivate, shared, default, |
master |
Директива для выделения программного кода в параллельном фрагменте, исполняемого только основным ( master ) потоком (п. 7.7.1). |
|
Директива для определения |
|
Директива для барьерной |
|
Директива для определения атомарной (неделимой) операции (п. 7.5.1). |
|
Директива для |
threadprivate (list) |
Директива для определения постоянных локальных переменных потоков (п. 7.7.4). |
ordered |
Директивы управления порядком вычислений в распараллеливаемом цикле (п. 7.3.2). При использовании данной директивы в директиве for должен быть указан одноименный параметр ordered |
Для справки приведем перечень параметров директив
| Параметр | Описание |
|---|---|
private (list)
|
Параметр для создания локальных копий для перечисленных в списке переменных для каждого имеющегося потока (п. 7.4.1). Исходные значения копий не определены. Директивы: parallel, for, sections, single |
firstprivate ( list ) |
Тоже что и параметр private и дополнительно инициализация создаваемых копий значениями, которые имели перечисленные в списке переменные перед началом параллельного фрагмента (п. 7.4.1). Директивы: parallel, for, sections, single |
lastprivate (list)
|
Тоже что и параметр private и дополнительно запоминание значений локальных переменных после завершения параллельного фрагмента (п. 7.4.1). Директивы: for, sections |
shared ( list ) |
Параметр для определения общих переменных для всех имеющихся потоков (п. 7.4.1). Директивы: parallel |
default ( shared | none ) |
Параметр для установки правила по умолчанию на использование переменных в потоках (п. 7.4.1). Директивы: parallel |
( operator: list ) |
Параметр для задания операции parallel, for, sections |
|
Параметр для отмены синхронизации при завершении директивы. Директивы: for, sections, single |
if (expression) |
Параметр для задания условия, только при выполнении которого осуществляется создание параллельного фрагмента (п. 7.3.4). Директивы: parallel |
ordered |
Параметр для задания порядка вычислений в распараллеливаемом цикле (п. 7.3.2). Директивы: for |
schedule (type [, |
Параметр для управления распределением итераций распараллеливаемого цикла между потоками (п. 7.3.1). Директивы: for |
copyin (list) |
Параметр для инициализации постоянных переменных потоков (п. 7.7.4). Директивы: parallel |
copyprivate (list) |
Копирование локальных переменных потоков после выполнения блока директивы single (п. 7.7.1). Директивы: single |
num_treads |
Параметр для задания количества создаваемых потоков в параллельной области (п. 7.7.5). Директивы: parallel |
Приведем для наглядности сводную таблицу использования параметров в директивах
| Параметр | Директива | |||||
|---|---|---|---|---|---|---|
parallel |
for |
sections |
single |
parallel
for |
parallel
sections |
|
if |
( | ( | ( | |||
private |
( | ( | ( | ( | ( | ( |
shared |
( | ( | ( | ( | ||
default |
( | ( | ( | |||
firstprivate |
( | ( | ( | ( | ( | ( |
lastprivate |
( | ( | ( | ( | ||
|
( | ( | ( | ( | ( | |
copyin |
( | ( | ( | |||
schedule |
( | ( | ||||
ordered |
( | ( | ||||
|
( | ( | ( | |||
num_threads |
( | ( | ||||
copyprivate |
( | |||||
Для справки приведем перечень функций библиотеки
| Функция | Описание |
|---|---|
void omp_set_num_threads (int num_threads) |
Установить количество создаваемых потоков (п. 7.7.5) |
int omp_get_max_threads (void) |
Получение максимально-возможного количества потоков (п. 7.7.5) |
int omp_get_num_threads (void) |
Получение количества потоков в параллельной области программы (п. 7.7.5) |
int omp_get_thread_num (void) |
Получение номера потока (п. 7.7.5) |
int omp_get_num_procs (void) |
Получение числа |
void omp_set_dynamic (int dynamic) |
Установить режим динамического создания потоков (п. 7.7.6) |
int omp_get_dynamic (void) |
Получение состояние динамического режима (п. 7.7.6) |
void omp_set_nested (int |
Установить режим поддержки вложенных параллельных фрагментов (п. 7.7.7) |
int omp_get_nested (void) |
Получения состояние режима поддержки вложенных параллельных фрагментов (п. 7.7.7) |
void omp_init_lock (omp_lock_t *lock) void omp_init_nest_lock(omp_nest_lock_t *lock) |
Инициализировать замок (п. 7.5.3) |
void omp_set_lock (omp_lock_t lock) void omp_set_nest_lock (omp_nest_lock_t lock) |
Установить замок (п. 7.5.3) |
void omp_unset_lock (omp_lock_t lock) void omp_unset_nest_lock (omp_nest_lock_t lock) |
Освободить замок (п. 7.5.3) |
int omp_test_lock (omp_lock_t lock) int omp_test_nest_lock (omp_nest_lock_t lock) |
Установить замок без блокировки (п. 7.5.3) |
void omp_destroy_lock(omp_lock_t lock) void omp_destroy_nest_lock(omp_nest_lock_t lock) |
Перевод замка в неинициализированное состояние (п. 7.5.3) |
double omp_get_wtime (void) |
Получение времени текущего момента выполнения программы (п. 7.2.5) |
double omp_get_wtick (void) |
Получение времени в секундах между двумя последовательными показателями времени аппаратного таймера (п. 7.2.5) |
int omp_in_parallel (void) |
Проверка нахождения программы в параллельном фрагменте |
Для справки приведем перечень переменных окружения
| Переменная | Описание |
|---|---|
OMP_SHEDULE |
Переменная для задания способа управления распределением итераций распараллеливаемого цикла между потоками (п. 7.3.1).
Значение по умолчанию: static |
OMP_NUM_THREADS |
Переменная для задания количество потоков в параллельном фрагменте (п. 7.7.5).
Значение по умолчанию: количество . |
OMP_DYNAMIC |
Переменная для задания динамического режима создания потоков (п. 7.7.6).
Значение по умолчанию: false. |
OMP_NESTED |
Переменная для задания режима вложенности параллельных фрагментов (п. 7.7.7).
Значение по умолчанию: false. |
При использовании многопроцессорных вычислительных систем с общей памятью обычно предполагается, что имеющиеся в составе системы процессоры обладают равной производительностью, являются равноправными при доступе к общей памяти, и время доступа к памяти является одинаковым (при одновременном доступе нескольких процессоров к одному и тому же элементу памяти очередность и синхронизация доступа обеспечивается на аппаратном уровне).
Перечисленному выше набору предположений удовлетворяют также активно развиваемые в последнее время многоядерные процессоры, в которых каждое ядро представляет практически независимо функционирующее вычислительное устройство. Для общности излагаемого учебного материала для упоминания одновременно и мультипроцессоров и многоядерных процессоров для обозначения одного вычислительного устройства (одноядерного процессора или одного
(рис 7.1) Архитектура многопроцессорных систем с общей (разделяемой) с однородным доступом памятью (для примера каждый процессор имеет два вычислительных ядра)Следует отметить, что общий доступ к данным может быть обеспечен и при физически распределенной памяти (при этом, естественно, длительность доступа уже не будет одинаковой для всех элементов памяти). Такой подход именуется как неоднородный доступ к памяти ( non-uniform memory access or NUMA ).
В самом общем виде системы с общей памятью могут быть представлены в виде модели
Обычный подход при организации вычислений для многопроцессорных вычислительных систем с общей памятью - создание новых параллельных методов на основе обычных последовательных программ, в которых или автоматически компилятором, или непосредственно программистом выделяются участки независимых друг от друга вычислений. Возможности автоматического анализа программ для порождения параллельных вычислений достаточно ограничены, и второй подход является преобладающим. При этом для разработки параллельных программ могут применяться как новые алгоритмические языки, ориентированные на параллельное программирование, так и уже имеющиеся языки программирования, расширенные некоторым набором операторов для параллельных вычислений.
Широко используемый подход состоит и в применении тех или иных библиотек, обеспечивающих определенный программный интерфейс ( application programming interface, API ) для разработки параллельных программ. В рамках такого подхода наиболее известны Windows Thread API (см., например, Вильямс (2001)) и PThead API (см., например, Butenhof (1997)). Однако первый способ применим только для ОС семейства Microsoft Windows, а второй вариант API является достаточно трудоемким для использования и имеет низкоуровневый характер.
Все перечисленные выше подходы приводят к необходимости существенной переработки существующего программного обеспечения, и это в значительной степени затрудняет широкое распространение параллельных вычислений. Как результат, в последнее время активно развивается еще один подход к разработке параллельных программ, когда указания программиста по организации параллельных вычислений добавляются в программу при помощи тех или иных внеязыковых средств языка программирования - например, в виде директив или комментариев, которые обрабатываются специальным препроцессором до начала компиляции программы. При этом исходный текст программы остается неизменным, и по нему, в случае отсутствия препроцессора, компилятор построит исходный последовательный программный код. Препроцессор же, будучи примененным, заменяет директивы параллелизма на некоторый дополнительный программный код (как правило, в виде обращений к процедурам какой-либо параллельной библиотеки).
Рассмотренный выше подход является основой технологии OpenMP (см., например, Chandra et al. (2000)), наиболее широко применяемой в настоящее время для организации параллельных вычислений на многопроцессорных системах с общей памятью. В рамках данной технологии директивы параллелизма используются для выделения в программе параллельных фрагментов, в которых последовательный исполняемый код может быть разделен на несколько раздельных командных потоков ( threads ). Далее эти потоки могут исполняться на разных процессорах (
(рис 7.2) Общая схема выполнения параллельной программы при использовании технологии OpenMPПри разработке технологии
Далее в разделе будет приведено последовательное описание возможностей технологии
Перед началом практического изучения технологии
Под параллельной программой в рамках
Важно отметить, что разделение вычислений между потоками осуществляется под управлением соответствующих директив
Потоки могут выполняться на разных процессорах (
Количество потоков определяется в начале выполнения параллельных фрагментов программы и обычно совпадает с количеством имеющихся np-1, где np есть общее количество потоков. Номер потока также может быть получен при помощи функции
Использование в технологии
Как уже отмечалось ранее, потоки исполняются в
n=n+1;
для общей переменной n. Тогда в зависимости от условий выполнения данная операция может быть выполнена поочередно (что приведет к получению правильного результата) или же оба потока могут одновременно прочитать значение переменной n, одновременно увеличить и записать в эту переменную новое значение (как результат, будет получено неправильное значение). Подобная ситуация, когда результат вычислений зависит от темпа выполнения потоков, получил наименование гонки потоков ( ). Для исключения гонки необходимо обеспечить, чтобы изменение значений общих переменных осуществлялось в каждый момент времени только одним единственным потоком - иными словами необходимо обеспечить взаимное исключение ( ) потоков при работе с общими данными. В ) операций, механизма кри
тических секций ( ) или специального типа семафоров - замков ( locks ).
Следует отметить, что организация взаимного исключения приводит к уменьшению возможности параллельного выполнения потоков - при одновременном доступе к общим переменным только один из них может продолжить работу, все остальные потоки будут блокированы и будут ожидать освобождения общих данных. Можно сказать, что именно при реализации взаимодействия потоков проявляется искусство параллельного программирования для вычислительных систем с общей памятью - организация взаимного исключения при работе с общими данными является обязательной, но возникающие при этом задержки (блокировки) потоков должны быть минимальными по времени.
Помимо взаимоисключения, при параллельном выполнении программы во многих случаях является необходимым та или иная синхронизация ( synchronization ) вычислений, выполняемых в разных потоках: например, обработка данных, выполняемая в одном потоке, может быть начата только после того, как эти данные будут сформированы в другом потоке (классическая задача параллельного программирования " производитель-потребитель " - "producer- ). В .
Конструктивно в составе технологии
Именно в таком порядке и будут рассмотрены возможности технологии
Стандарт предусматривает использование
В самом общем виде формат директив
#pragma omp <имя_директивы> [<параметр>[[,] <параметр>]…]
Начальная часть директивы (#clause ).
Для иллюстрации приведем пример директивы:
#pragma omp parallel default(shared) \
private(beta,pi)
Пример показывает также, что для задания директивы может быть использовано несколько строк программы - признаком наличия продолжения является знак обратного слеша "\".
Действие директивы распространяется, как правило, на следующий в программе оператор, который может быть, в том числе, и структурированным блоком.
Итак, параллельная программа, разработанная с использованием
Для выделения параллельных фрагментов программы следует использовать директиву parallel:
#pragma omp parallel [<параметр> ...] <блок_программы>
Для блока (как и для блоков всех других директив
Директива parallel является одной из основных директив
parallel, создается набор ( team ) из N потоков; исходный поток программы является основным потоком этого набора ( master thread ) и имеет номер 0.Подчеркнем чрезвычайно важный момент - оказывается, даже такого краткого рассмотрения возможностей технологии
#include <omp.h>
main () {
/* Выделение параллельного фрагмента*/
#pragma omp parallel
{
printf("Hello World !\n");
}/* Завершение параллельного фрагмента */
}
В приведенной программе файл omp.h содержит определения parallel будут созданы потоки (по умолчанию их количество совпадает с числом имеющихся
После рассмотрения примера важно отметить также, что параллельное выполнение программы будет осуществляться не только для программного блока, непосредственно следующего за директивой parallel, но и для всех функций, вызываемых из этого блока (см. рис. 7.3). Для обозначения этих динамически-возникающих параллельно выполняемых участков программного кода в parallel region ) - ранее, в предшествующих версиях стандарта использовался термин динамический контекст ( dynamic ).
(рис 7.3) Область видимости директив OpenMPРяд директив ).
Для более точного понимания излагаемого учебного материала сведем воедино введенные термины и понятия (в скобках даются названия, используемые в стандарте 2.5):
parallel construct ) - блок программы, управляемый директивой parallel ; именно параллельные фрагменты, совместно с параллельными областями, представляют параллельно-выполняемую часть программы; в предшествующих стандартах для обозначения данного понятия использовался термин лексический контекст ( lexical extent ) директивы parallel.parallel region ) - параллельно выполняемые участки программного кода, динамически-возникающие в результате вызова функций из параллельных фрагментов - см. рис. 7.3.parallel section ) - часть параллельного фрагмента, выделяемая для параллельного выполнения при помощи директивы section - см. подраздел 7.6.Приведем перечень параметров директивы parallel:
if (scalar_expression)private (list) shared (list) default (shared | none) firstprivate (list) reduction (operator: list) copyin (list)num_threads (scalar_expression)Список параметров приведен только для справки, пояснения по использованию этих параметров будут даны позднее по мере изложения учебного материала.
Практически сразу после разработки первой параллельной программы появляется необходимость определения времени выполнения вычислений, поскольку в большинстве случаев основной целью использования параллельных вычислительных систем является сокращений времени выполняемых расчетов. Используемые обычно средства для измерения времени работы программ зависят, как правило, от аппаратной платформы, операционной системы,
Получение текущего момента времени выполнения программы обеспечивается при помощи функции:
double omp_get_wtime(void),
результат вызова которой есть количество секунд, прошедших от некоторого определенного момента времени в прошлом. Этот момент времени в прошлом, от которого происходит отсчет секунд, может зависеть от среды реализации omp_get_wtime следует использовать только для определения длительности выполнения тех или иных фрагментов кода параллельных программ. Возможная схема применения функции omp_get_wtime может состоять в следующем:
double t1, t2, dt; t1 = omp_get_wtime (); … t2 = omp_get_wtime (); dt = t2 - t1;
Точность измерения времени также может зависеть от среды выполнения параллельной программы. Для определения текущего значения точности может быть использована функция:
double omp_get_wtick(void),
позволяющая определить время в секундах между двумя последовательными показателями времени аппаратного таймера используемой компьютерной системы.
Как уже отмечалось ранее, программный код блока директивы parallel по умолчанию исполняется всеми потоками. Данный способ может быть полезен, когда нужно выполнить одни и те же действия многократно (как в примере 7.1) или когда один и тот же программный код может быть применен для выполнения обработки разных данных. Последний вариант достаточно широко используется при разработке parallel - можно осуществить разделение итеративно-выполняемых действий в циклах для непосредственного указания, над какими данными должными выполняться соответствующие вычисления. Такая возможность является тем более важной, поскольку
во
многих случаях именно в циклах выполняется основная часть вычислительно-трудоемких вычислений.
Для распараллеливания циклов в for:
#pragma omp for [<параметр> ...] <цикл_for>
После этой директивы итерации цикла распределяются между потоками и, как результат, могут быть выполнены параллельно (см. рис. 7.4) - понятно, что такое распараллеливание возможно только, если между итерациями цикла нет информационной зависимости.
(рис 7.4) Общая схема распараллеливания цикловВажно отметить, что для распараллеливания цикл for должен иметь некоторый "канонический" тип цикла со
for (index = first; index < end; increment_expr)
Здесь index должен быть целой переменной; на месте знака " < "
в выражении для проверки окончания цикла может находиться любая операция сравнения
" <= ", " > " или " >= ".
Операция изменения переменной цикла должна иметь одну из следующих форм:
index++, ++index,index--, --index,index+=incr, index-=incr,index=index+incr, index=incr+index,index=index-incrИ, конечно же, переменные, используемые в заголовке
В качестве примера использования директивы рассмотрим учебную задачу вычисления суммы элементов для каждой строки прямоугольной матрицы:
#include <omp.h>
#define CHUNK 100
#define NMAX 1000
main () {
int i, j, sum;
float a[NMAX][NMAX];
<инициализация данных>
#pragma omp parallel shared(a) private(i,j,sum)
{
#pragma omp for
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=0; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
} /* Завершение параллельного фрагмента */
}
В приведенной программе для директивы parallel появились два параметра - их назначение будет описано в следующем подразделе, здесь же отметим, что параметры директивы shared и private определяют доступность данных в потоках программы - переменные, описанные как shared, являются общими для потоков; для переменных с описанием private создаются отдельные копии для каждого потока, эти локальные копии могут использоваться в потоках независимо друг от друга.
Следует отметить, что если в блоке директивы parallel нет ничего, кроме директивы for, то обе директивы можно объединить в одну, т.е. пример 7.2 может быть переписан в виде:
#include <omp.h>
#define NMAX 1000
main () {
int i, j, sum;
float a[NMAX][NMAX];
<инициализация данных>
#pragma omp parallel for shared(a) private(i,j,sum)
{
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=0; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
} /* Завершение параллельного фрагмента */
}
Параметрами директивы for являются:
schedule (type [,chunk ])orderednowait private (list) shared (list)firstprivate (list)lastprivate (list)reduction (operator: list)Последние пять параметров директивы будут рассмотрены в следующем подразделе, здесь же приведем описание оставшихся параметров.
При разном объеме вычислений в разных итерациях цикла желательно иметь возможность управлять распределением итераций цикла между потоками - в schedule директивы for. Поле type параметра schedule может принимать следующие значения:
static - статический способ распределения итераций до начала выполнения цикла. Если поле chunk не указано, то итерации делятся поровну между потоками. При заданном значении chunk итерации цикла делятся на блоки размера chunk и эти блоки распределяются между потоками до начала выполнения цикла.dynamic - динамический способ распределения итераций. До начала выполнения цикла потокам выделяются блоки итераций размера chunk (если поле chunk не указано, то полагается значение chunk =1 ). Дальнейшее выделение итераций (также блоками размера chunk ) осуществляется в момент завершения потоками своих ранее назначенных итераций.guided - управляемый способ распределения итераций. Данный способ близок к предшествующему варианту, отличие состоит только в том, что начальный размер блоков итераций определяется в соответствии с некоторым параметром среды реализации chunk есть некоторая доля предшествующего значения) при каждом новом выделении блока итераций. При этом получаемый размер блока итераций не должен быть меньше значения chunk (если поле chunk не указано, то полагается значение chunk =1 ).runtime - способ распределения OMP_SCHEDULE. Так, например, для задания динамического способа при размере блока итераций 3, следует определить:setenv OMP_SCHEDULE "dynamic,3"
Полезность такого варианта очевидна - способ распределения итераций между потоками можно менять, не корректируя при этом код программы (т.е. без повторной компиляции и сборки программы).
Для демонстрации примера использования параметра schedule предположим, что матрица в примере 7.3 имеет верхний треугольный вид - в этом случае объем вычислений для каждой строки является различным и последовательное распределение итераций поровну приведет к неравномерному распределению вычислительной нагрузки между потоками. Для балансировки расчетов можно применить статическую или динамическую схемы распределения итераций:
#include <omp.h>
#define CHUNK 100
#define NMAX 1000
main () {
int i, j, sum;
float a[NMAX][NMAX];
<инициализация данных>
#pragma omp parallel for shared(a) private(i,j,sum) \
schedule (dynamic, CHUNK)
{
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=i; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
} /* Завершение параллельного фрагмента */
}
В результате распараллеливания цикла порядок выполнения итераций не фиксирован: в зависимости от состояния среды выполнения очередность выполнения итераций может меняться. Если же для ряда действий в цикле необходимо сохранить первичный порядок вычислений, который соответствует последовательному выполнению итераций в последовательной программе, то желаемого результата можно добиться при помощи директивы ordered (при этом для директивы for должен быть указан параметр ordered ). Поясним сказанное на примере нашей учебной задачи. Для приведенного выше варианта программы печать сумм элементов строк матрицы будет происходить в некотором произвольном порядке; при необходимости печати по порядку расположения строк следует провести следующее изменение программного кода:
#pragma omp parallel for shared(a) private(i,j,sum) \
schedule (dynamic, CHUNK) ordered {
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=i; j < NMAX; j++)
sum += a[i][j];
#pragma omp ordered
printf ("Сумма элементов строки %d равна %f\n",i,sum);
} /* Завершение параллельного фрагмента */
Поясним дополнительно, что параметр ordered управляет порядком выполнения только тех действий, которые выделены директивой ordered - выполнение оставшихся действий в итерациях цикла по-прежнему может происходить параллельно. Важно помнить также, что директива ordered может быть применена в теле цикла только один раз.
Следует отметить, что указание необходимости сохранения порядка вычислений может привести к задержкам при
По умолчанию, все потоки, прежде чем перейти к выполнению дальнейших вычислений, ожидают окончания выполнения итераций цикла даже если некоторые из них уже завершили свои вычисления - конец цикла представляет собой некоторый барьер, который потоки могут преодолеть только все вместе. Можно отменить указанную синхронизацию, указав параметр в директиве for - тогда потоки могут продолжить вычисления за переделами цикла, если для них нет итераций цикла для выполнения.
Теперь при наличии учебного примера можно пояснить назначение параметра if директивы parallel.
При разработке if директивы parallel, задавая при его помощи условие создания параллельного фрагмента (если условие параметра if не выполняется, блок директивы parallel выполняется как обычный последовательный код). Так, например, в нашем учебном примере можно ввести условие, определяющее минимальный размер матрицы, при котором осуществляется
#include <omp.h>
#define NMAX 1000
#define LIMIT 100
main () {
int i, j, sum;
float a[NMAX][NMAX];
<инициализация данных>
#pragma omp parallel for shared(a) private(i,j,sum) if (NMAX>LIMIT)
{
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=0; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
} /* Завершение параллельного фрагмента */
}
В приведенном примере параллельный фрагмент создается только, если порядок матрицы превышает 100 (т.е. когда обеспечивается некоторый минимальный объем выполняемых вычислений).
Как уже отмечалось ранее, потоки параллельной программы выполняются в
Параметры shared и private директивы for для управления доступа к переменным уже использовались в примере 7.2. Параметр shared определяет переменные, которые будут общими для всех потоков. Параметр private указывает переменные, для которых в каждом потоке будут созданы локальные копии - они будут доступны только внутри каждого потока в отдельности (значения локальных переменных потока недоступны для других потоков). Параметры shared и private могут повторяться в одной и той же директиве несколько раз, имена переменных должны быть уже ранее определены и не могут повторяться в списках параметров shared и private.
По умолчанию все переменные программы являются общими. Такое соглашение приводит к тому, что компилятор не может указать на ошибочные ситуации, когда программисты забывают описывать локальные переменные потоков в параметре private (отсутствие таких описаний приводит к тому, что переменные будут восприниматься как глобальные). Для выявления таких ошибок можно воспользоваться параметром default директивы parallel для изменения правила по умолчанию:
default ( shared | none )
Как можно видеть, при помощи этого параметра можно отменить действие правила по умолчанию ( default(none) ) или восстановить правило, что по умолчанию переменные программы являются общими ( default(shared) ).
Следует отметить, что начальные значения локальных переменных не определены, а конечные значения теряются при завершении потоков. Для инициализации можно использовать параметр firstprivate директивы for, по которому начальные значения локальных переменных будут устанавливаться в значения, которые существовали в переменных до момента создания локальных копий. Запоминание конечных значений обеспечивается при помощи параметра lastprivate, в соответствии с которым значения локальных переменных копируются из потока, выполнившего последнюю итерацию. Поясним сказанное на примере рис. 7.5. На рисунке показана переменная sum, которая определена как lastprivate в директиве parallel for. Для этой переменной создаются локальные копии в каждом потоке, при завершении параллельного участка программного кода значение локальной переменной потока, выполнившего последнюю итерацию цикла, переписывается в исходную п
еременную sum.
(рис 7.5) Общая схема работы с локальными переменными потоков
Использование параметра lastprivate позволяет сохранить значений локальной переменной одного из потоков, но во многих случаях для обработки могут понадобиться значения всех локальных переменных. Данная возможность может быть обеспечена, например, сохранением этих значений в общих переменных - более подробно правила работы с общими переменными будет рассмотрена в следующем подразделе. Другой подход состоит в использовании коллективных операций над локальными переменными, предусмотренными в директивы for:
reduction (operator: list)
где список list задает набор локальных переменных (повторное описание в параметре private переменных из списка list не требуется), для которых должна быть выполнена коллективная операция, а поле operator указывает тип этой коллективной операции. Допустимыми значениями для поля operator являются следующие операции (которые не могут быть перегружены):
+, -, *, , |, ^, , ||
Операция
x = x <operator> <expression>x = <expression> <operator> x (за исключением операции вычитания)x <op >= <expression>x++, ++x, x--, --xгде x есть имя скалярной переменной, выражение expression не должно включать переменную x, возможные операции для поля operator совпадают с выше приведенным списком, а допустимыми значениями для поля являются операции:
+, -, *, , |, ^
В качестве примера можно добавить в нашу учебную задачу действие по сложению всех сумм элементов строк матрицы - возможный программный код может быть следующим:
total = 0;
#pragma omp parallel for shared(a) private(i,j,sum) reduction (+:total)
{
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=i; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
total = total + sum;
} /* Завершение параллельного фрагмента */
printf ("Общая сумма элементов матрицы равна %f\n",total);
В качестве пояснений добавим, что по параметру для переменной total создаются локальные переменные, затем полученные значения в потоках суммируются и запоминаются в исходной переменной. Подчеркнем, использование общей переменной total без создания локальных копий для накопления общей суммы при использовании в потоках операции
total = total + sum;
является неправильным, поскольку без обеспечения взаимоисключения при использовании общей переменной возникает ситуация гонки потоков и итоговый результат может быть ошибочным (см. следующий подраздел).
Как уже неоднократно отмечалось ранее, при изменении общих данных несколькими потоками должны быть обеспечены условия взаимоисключения - изменение значений общих переменных должно осуществляться в каждый конкретный момент времени только одним потоком. Рассмотрим возможные способы организации взаимоисключения.
Действие над общей переменной может быть выполнено как атомарная (неделимая) операция при помощи директивы . Формат директивы имеет вид:
#pragma omp atomic <expression>
где expression должно иметь вид:
x++ ; или ++x ; или x-- ; или --x;
где x есть имя любой целой скалярной переменной.
Директива может быть записана и в виде:
#pragma omp atomic x <operator>= <expression>
где x есть имя скалярной переменной, выражение expression не должно включать переменную x, а допустимыми значениями для поля operator являются следующие операции (которые не могут быть перегружены):
+, *, -, /, , |, ^, >>, <<
Как следует из названия, операция директивы выполняется как неделимое действие над указанной общей переменной, и, как результат, никакие другие потоки не могут получить доступ к этой переменной в этот момент времени.
Применим рассмотренную директиву для нашей учебной задачи при вычислении общей суммы:
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);
Отметим, что в данном случае total.
Как можно видеть, директива может быть применена только для простых выражений, но является наиболее эффективным средством организации взаимоисключения, поскольку многие из допустимых для директивы операций на самом деле выполняются как атомарные на аппаратном уровне. Тем не менее, следует отметить, что данный вариант программы в общем случае будет проигрывать варианту 7.6 по эффективности, поскольку теперь
Действия над общими переменными могут быть организованы в виде критической секции, т.е. как блок программного кода, который может выполняться только одним потоком в каждый конкретный момент времени. При попытке входа в критическую секцию, которая уже исполняется одним из потоков используется, все другие потоки приостанавливаются ( блокируются ). Как только критическая секция освобождается, один из приостановленных потоков (если они имеются) активизируется для выполнения
Определение , формат записи которой имеет вид:
#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);
Следует обратить внимание на реализацию проверки суммы элементов строки на максимум. Директиву можно записать до первого оператора if, однако это приведет к тому, что критическая секция будет задействована для каждой строки и это приведет к дополнительным блокировкам потоков. Лучший вариант - организовать критическую секцию только тогда, когда необходимо осуществить запись в общую переменную smax (т.е. когда сумма элементов строки превышает значение максимума). Отметим особо, что после входа в критическую секцию необходимо повторно проверить переменную sum на максимум, поскольку после первого оператора if и до входа в критическую секцию значение smax может быть изменено другими потоками. Отсутствие второго оператора if приведет к появлению трудно-выявляемой ошибки, учет подобных моментов представляет определенную трудность параллельного программирования.
В omp_lock_t, который близок к классическому понятию семафоров. Для переменных этого типа определены функции библиотеки
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);
В ), т.е. их использование обеспечивается при помощи функций:
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.
Напомним, что в рассмотренном ранее учебном материале для построения параллельных программ был изложен подход (см. подразделы 7.2 - 7.3), обычно именуемый распараллеливанием по данным, в рамках которого обеспечивается одновременное (параллельное) выполнение одних и тех же вычислительных действий над разными наборами обрабатываемых данных (например, как в нашем учебном примере, суммирование элементов разных строк матрицы). Другая также широко встречающаяся ситуация состоит в том, что для решения поставленной задачи необходимо выполнить разные процедуры обработки данных, при этом данные процедуры или полностью не зависят друг от друга, или же являются слабо связанными. В этом случае такие процедуры можно выполнить параллельно; такой подход обычно именуется распараллеливанием по задачам. Для поддержки такого способа организации п
араллельных вычислений в parallel, можно выделять параллельно выполняемые программные секции (директива sections ).
Формат директивы sections имеет вид:
#pragma omp sections [<параметр> ...]
{
#pragma omp section
<блок_программы>
#pragma omp section
<блок_программы>
}
При помощи директивы sections выделяется программный код, который далее будет разделен на параллельно выполняемые секции. Директивы section определяют секции, которые могут быть выполнены параллельно (для первой по порядку секции директива section не является обязательной) - см. рис. 7.6. В зависимости от взаимного сочетания количества потоков и количества определяемых секций, каждый поток может выполнить одну или несколько секций (вместе с тем, при малом количестве секций некоторые потоки могут оказаться и без секций и окажутся незагруженными). Как результат, можно отметить, что использование секций достаточно сложно поддается масштабированию (настройке на число имеющихся потоков). Кроме того, важно помнить, что в соответствии со стандартом, порядок выполнения программных секций не определен, т.е. секции могут быть выполнены потоками в произвольном порядке.
(рис 7.6) Общая схема выполнения параллельных секций директивы sectionsВ качестве примера использования директивы расширим нашу учебную задачу действиями по копированию обрабатываемой матрицы (в качестве исходного варианта используется пример 7.6):
total = 0;
#pragma omp parallel shared(a,b) private(i,j)
{
#pragma omp sections /* Вычисление сумм элементов строк и общей суммы */
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=i; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
total = total + sum;
}
#pragma omp section
/* Копирование матрицы */
for (i=0; i < NMAX; i++) {
for (j=i; j < NMAX; j++)
b[i][j] = a[i][j];
}
} /* Завершение параллельного фрагмента */
printf ("Общая сумма элементов матрицы равна %f\n",total);
Для обсуждения примера можно отметить, что выполняемые в программе вычисления (суммирование и копирование элементов) можно было бы объединить в рамках одних и тех же циклов, однако приведенное разделение тоже может оказаться полезным, поскольку в результате разделения данные вычисления могут быть легко векторизованы (конвейеризированы) компилятором. Важно отметить также, что сформированные подобным образом программные секции не сбалансированы по вычислительной нагрузке (первая секция содержит больший объем вычислений).
В качестве параметров директивы sections могут использоваться:
private (list) firstprivate (list) lastprivate (list) reduction (operator: list) nowait Все перечисленные параметры уже были рассмотрены ранее. Отметим, что по умолчанию выполнение директивы sections синхронизировано, т.е. потоки, завершившие свои вычисления, ожидают окончания работы всех потоков для одновременного завершения директивы.
В заключение можно добавить, что также как и для директивы for, в случае если в блоке директивы parallel присутствует только директива sections, то данные директивы могут быть объединены. С использованием данной возможности пример 7.11 может быть переработан к виду:
total = 0;
#pragma omp parallel sections shared(a,b) private(i,j)
{
/* Вычисление сумм элементов строк и общей суммы */
for (i=0; i < NMAX; i++) {
sum = 0;
for (j=i; j < NMAX; j++)
sum += a[i][j];
printf ("Сумма элементов строки %d равна %f\n",i,sum);
total = total + sum;
}
#pragma omp section
/* Копирование матрицы */
for (i=0; i < NMAX; i++) {
for (j=i; j < NMAX; j++)
b[i][j] = a[i][j];
}
} /* Завершение параллельного фрагмента */
printf ("Общая сумма элементов матрицы равна %f\n",total);
При выполнении параллельных фрагментов может оказаться необходимым реализовать часть программного кода только одним потоком (например, открытие файла). Данную возможность в single и master.
Формат директивы single имеет вид:
#pragma omp single [<параметр> ...] <блок_программы>
Директива single определяет блок параллельного фрагмента, который должен быть выполнен только одним потоком; все остальные потоки ожидают завершения выполнения данного блока (если не указан параметр ) - см. рис. 7.7.
(рис 7.7) Общая схема выполнения директивы singleВ качестве параметров директивы могут использоваться:
private (list)firstprivate (list) copyprivate (list)nowait Новым в приведенном списке является только параметр copyprivate, который обеспечивает копирование переменных, указанных в списке list, после выполнения блока директивы single в локальные переменные всех остальных потоков.
Формат директивы master имеет вид:
#pragma omp master <блок_программы>
Директива master определяет фрагмент кода, который должен быть выполнен только основным потоком; все остальные потоки пропускают данный фрагмент
При помощи директивы можно определить точку синхронизации, которую должны достигнуть все потоки для продолжения вычислений (директива может находиться в пределах как параллельного фрагмента так и параллельной области, т.е. директива является отделяемой).
Формат директивы имеет вид:
#pragma omp barrier
Директива позволяетопределить точку синхронизации, в которой системой должно быть обеспечено единое для всех потоков состояние памяти (т.е. если потоком какое-либо значение извлекалось из памяти для модификации, измененное значение обязательно должно быть записано в
Формат директивы имеет вид:
#pragma omp flush [(list)]
Как показывает формат, директива содержит список list с перечнем переменных, для которых выполняется синхронизация; при отсутствии списка синхронизация выполняется для всех переменных потока.
Следует отметить, что директива неявным образом присутствует в директивах .
Как описывалось при рассмотрении директивы parallel, для потоков могут быть определены локальные переменные (при помощи параметров private, firstprivate, lastprivate ), которые создаются в начале соответствующего параллельного фрагмента и удаляются при завершении потоков. В threadprivate.
Формат директивы threadprivate имеет вид:
#pragma omp threadprivate (list)
Список list определяет набор определяемых переменных. Созданные локальные копии не видимы в последовательных участках выполнения программы (т.е. вне параллельных фрагментов), но существуют в течение всего времени выполнения программы. Указываемые в списке переменные должны быть уже определены в программе; объявление переменных в директиве должно предшествовать использованию переменных в потоках.
Следует отметить, что использование директивы threadprivate позволяет решить еще одну проблему. Дело в том, что действие параметров private распространяется только на программный код параллельных фрагментов, но не параллельных областей - т.е., например, любая локальная переменная, определенная в параллельном фрагменте функции root на рис. 7.3, будет недоступна в параллельной области функции node. Выход из такой ситуации может быть или в передаче значений локальных переменных через параметры функций или же в использовании постоянных локальных переменных директивы threadprivate.
Отметим еще раз, что полученные в потоках значения постоянных переменных сохраняются между параллельными фрагментами программы. Значения этих переменных можно переустановить при начале параллельного фрагмента по значениям из основного потока при помощи параметра copyin директивы parallel.
Формат параметра copyin директивы parallel имеет вид:
copyin (list)
Для демонстрации рассмотренных понятий приведем пример A.32.1c из стандарта
#include <stdlib.h>
float* work;
int size;
float tol;
#pragma omp threadprivate(work,size,tol)
void a32( float t, int n )
{
tol = t;
size = n;
#pragma omp parallel copyin(tol,size)
{
build();
}
}
void build()
{
int i;
work = (float*)malloc( sizeof(float)*size );
for( i = 0; i < size; ++i ) work[i] = tol;
}
В приведенном примере определяются постоянно существующие локальные переменные work, size, tol для потоков (директива threadprivate ). Перед началом параллельного фрагмента значения этих переменных из основного потока копируются во все потоки (параметр copyin ). Значения постоянно существующих локальных переменных доступны в параллельной области функции build (для обычных локальных переменных потоков их значения пришлось бы передавать через параметры функции).
По умолчанию количество создаваемых потоков определяется реализацией и обычно совпадает с числом имеющихся
Прежде всего, количество создаваемых потоков может быть задано при помощи параметра num_threads директивы parallel.
Количество необходимых потоков может быть также задано при помощи функции omp_set_num_threads библиотеки
Формат функции omp_set_num_threads имеет вид:
void omp_set_num_threads (int num_threads);
Вызов функции omp_set_num_threads должен осуществляться из последовательной части программы.
Для задания необходимого количества потоков можно воспользоваться и переменной окружения OMP_NUM_THREADS. При использовании нескольких способов задания наибольший приоритет имеет параметр num_threads директивы parallel, затем функция библиотеки, затем переменная окружения.
Изменение количества создаваемых потоков может быть полезно и для целей отладки разрабатываемой параллельной программы для проверки ее работоспособности при разном количестве потоков.
Приведем здесь также дополнительные функции библиотеки
int omp_get_max_threads(void)
int omp_get_num_threads(void)
int omp_get_thread_num(void)
int omp_get_num_procs(void)
Количество создаваемых потоков в параллельных фрагментах программы по умолчанию является фиксированным (см. также предыдущий пункт), однако стандартом предусматривается возможность динамического режима, когда количество потоков может определяться реализацией для оптимизации функционирования вычислительной системы. Разрешение динамического режима и его отключение осуществляется при помощи функции omp_set_dynamic библиотеки
Формат функции omp_set_ dynamic имеет вид:
void omp_set_dynamic (int dynamic);
Вызов функции omp_set_dynamic должен осуществляться из последовательной части программы. Функция разрешает ( dynamic=true )илиотключает ( dynamic=false )динамический режим создания потоков.
Для управления динамическим режимом создания потоков можно воспользоваться и переменной окружения OMP_DYNAMIC. При использовании нескольких способов задания наибольший приоритет имеет функция библиотеки, затем переменная окружения.
Для получения состояния динамического режима можно воспользоваться функций omp_get_dynamic библиотеки
int omp_get_dynamic (void);
Параллельные фрагменты программы могут быть вложенными - такая возможность определяется реализацией
Управление режимом выполнения вложенных фрагментов осуществляется при помощи функции omp_set_nested библиотеки
Формат функции omp_set_nested имеет вид:
void omp_set_nested (int nested);
Вызов функции omp_set_nested должен осуществляться из последовательной части программы. Функция разрешает (
Для управления режимом поддержки вложенных параллельных фрагментов можно воспользоваться и переменной окружения OMP_NESTED. При использовании нескольких способов задания наибольший приоритет имеет функция библиотеки, затем переменная окружения.
Для получения состояния режима поддержки вложенных параллельных фрагментов можно воспользоваться функций omp_get_nested библиотеки
int omp_get_nested (void);
Итак, возможности технологии
Между разработкой параллельных программ с использованием
Прежде всего формат директив
<маркер> <имя_директивы> [<параметр>…]
Вид маркера зависит от формата записи программы:
!$omp или c$omp или *$omp
Маркер должен начинаться с позиции 1 строки. При этом сама директива может быть записана в несколько строк; первая строка директивы должна содержать в позиции 6 или пробел или 0; строки продолжения директивы в позиции 6 должны быть отмечены любым произвольным символом, отличающимся от пробела и 0.
!$omp. Маркер может быть записан, начиная с произвольной позиции строки, при этом маркеру должны предшествовать только пробелы или знаки табуляции. При размещении директивы на нескольких строках первая строка должна заканчиваться символом , строки продолжения могут начинаться с этого знака, но приводить его в строках продолжения не обязательно.В качестве основных отличий параллельных программ с использованием
end critical end do end master end ordered end parallel end sections end single end workshare
workshare, которая является близкой по своему назначению директиве sections.CALL.В качестве принятых соглашений при разработке программ на языке Fortran рекомендуется записывать имена подпрограмм с использованием прописных символов.
В качестве примера приведем вариант программы из примера 7.6 на
PROGRAM Example
INCLUDE "omp_lib.h"
INTEGER NMAX 1000
INTEGER LIMIT 100
INTEGER I, J, SUM
REAL A(NMAX,NMAX)
<инициализация данных>
!$OMP PARALLEL DO SHARED(A) PRIVATE(I,J,SUM) IF (NMAX>LIMIT)
DO I = 1, NMAX
SUM = 0
DO J = 1, NMAX
SUM = SUM + A(I,J)
ENDDO
PRINT * "Сумма элементов строки ", I, "равна ", SUM
ENDDO
!$OMP END PARALLEL DO ! Завершение параллельного фрагмента
STOP
END
При разработке параллельной программы с использованием
Поддержка единственности программного кода относится к числу важных положительных качеств технологии
Однако проблемы поддержки единственности программного кода возникают при использовании в параллельной программе функций и переменных окружения _OPENMP, значением которой является дата поддерживаемого стандарта в формате ГГГГММ (год, месяц). Как результат, для обеспечения единственности программного кода последовательного и параллельного вариантов программ, все
#ifdef _OPENMP <OpenMP-зависимый программный код> #endif
Последовательный вариант программы может порождаться и компиляторами с поддержкой
Среди компиляторов с поддержкой
| Параметр | Описание | |
|---|---|---|
| Windows | Linux | |
| -Qopenmp | - |
Компиляция программного кода с использованием |
| -Qopenmp-profile | - |
Компиляция с инструментацией программного кода с использованием |
| -Qopenmp- |
- |
Компиляция программного кода как обычной последовательной программы (с игнорированием директив и использованием функций-заглушек для функций |
Следует также помнить, что при сборке программы следует использовать библиотеки, обеспечивающие поддержки многопоточности.
Среди других компиляторов можно отметить:
Полный перечень компиляторов, поддерживающих
Данный раздел посвящен рассмотрению методов параллельного программирования для вычислительных систем с общей памятью с использованием технологии
В самом начале раздела отмечается, что технология threads ). Далее эти потоки могут исполняться на разных процессорах (
В подразделе 7.1 рассматривается ряд понятий и определений, являющихся основополагающими для стандарта
В подразделе 7.2 проводится быстрое и простое введение в разработку параллельных программ с использованием parallel и приводится пример первой параллельной программы с использованием
В подразделе 7.3 рассматриваются вопросы распределения вычислительной нагрузки между потоками на примере распараллеливания циклов по данным. Дается описание директивы for и описываются способы управления распределением итераций цикла между потоками.
В подразделе 7.4 подробно обсуждаются вопросы управления данными для параллельно выполняемых потоков. Дается понятие общих и локальных переменных для потоков. Приводится описание важной и часто встречающейся при обработке общих данных операции редукции.
В подразделе 7.5 рассматриваются вопросы организации взаимоисключения при использовании общих
В подразделе 7.6 излагаются вопросы распределения вычислительной нагрузки между потоками на основе распараллеливания задач. Дается описание директивы sections, приводятся примеры использования данной директивы.
В подразделе 7.7 рассматриваются расширенные возможности технологии master, single, , , threadprivate, copyin ), обсуждаются возможности управления потоками (количество создаваемых потоков, динамический режим создания потоков, вложенность параллельных фрагментов).
В завершающем подразделе 7.8 даются дополнительные сведения о технологии
В наиболее полном виде информация по параллельному программированию для вычислительных систем с общей памятью с использованием
Достаточно много информации о технологии
Для рассмотрения общих вопросов параллельного программирования для вычислительных систем с общей памятью может быть рекомендована работа Andrews (2000).
parallel?sections )?single и master )?flush )?threadprivate и copyin )?(описание методов интегрирования дано, например, в Kahaner, Moler and Nash (1988)).
,
reduction директивы for.sections.Для справки приведем перечень директив
| Директива | Описание |
|---|---|
parallel [<параметр>…] |
Директива определения параллельного фрагмента в коде программы (подраздел 7.2). Параметры: if, private, shared, default, firstprivate, |
for [<параметр>…] |
Директива распараллеливания циклов (подраздел 7.2). Параметры: private, firstprivate, lastprivate, |
sections [<параметр>…] |
Директива для выделения программного кода, который далее будет разделен на параллельно выполняемые параллельные секции (подраздел 7.6). Выделение параллельных секций осуществляется при помощи директивы section. Параметры: private, firstprivate, lastprivate, |
section |
Директива выделения параллельных секций - должна располагаться в блоке директивы sections (подраздел 7.6). |
single [<параметр>…] |
Директива для выделения программного кода в параллельном фрагменте, исполняемого только одним потоком (п. 7.7.1). Параметры: private, firstprivate, copyprivate, |
parallel for
[< параметр >… |
Объединенная форма директив parallel и for (подраздел 7.2). Параметры: private, firstprivate, lastprivate, shared, default, |
parallel sections [<параметр>… |
Объединенная форма директив parallel и sections (подраздел 7.6). Параметры: private, firstprivate, lastprivate, shared, default, |
master |
Директива для выделения программного кода в параллельном фрагменте, исполняемого только основным ( master ) потоком (п. 7.7.1). |
|
Директива для определения |
|
Директива для барьерной |
|
Директива для определения атомарной (неделимой) операции (п. 7.5.1). |
|
Директива для |
threadprivate (list) |
Директива для определения постоянных локальных переменных потоков (п. 7.7.4). |
ordered |
Директивы управления порядком вычислений в распараллеливаемом цикле (п. 7.3.2). При использовании данной директивы в директиве for должен быть указан одноименный параметр ordered |
Для справки приведем перечень параметров директив
| Параметр | Описание |
|---|---|
private (list)
|
Параметр для создания локальных копий для перечисленных в списке переменных для каждого имеющегося потока (п. 7.4.1). Исходные значения копий не определены. Директивы: parallel, for, sections, single |
firstprivate ( list ) |
Тоже что и параметр private и дополнительно инициализация создаваемых копий значениями, которые имели перечисленные в списке переменные перед началом параллельного фрагмента (п. 7.4.1). Директивы: parallel, for, sections, single |
lastprivate (list)
|
Тоже что и параметр private и дополнительно запоминание значений локальных переменных после завершения параллельного фрагмента (п. 7.4.1). Директивы: for, sections |
shared ( list ) |
Параметр для определения общих переменных для всех имеющихся потоков (п. 7.4.1). Директивы: parallel |
default ( shared | none ) |
Параметр для установки правила по умолчанию на использование переменных в потоках (п. 7.4.1). Директивы: parallel |
( operator: list ) |
Параметр для задания операции parallel, for, sections |
|
Параметр для отмены синхронизации при завершении директивы. Директивы: for, sections, single |
if (expression) |
Параметр для задания условия, только при выполнении которого осуществляется создание параллельного фрагмента (п. 7.3.4). Директивы: parallel |
ordered |
Параметр для задания порядка вычислений в распараллеливаемом цикле (п. 7.3.2). Директивы: for |
schedule (type [, |
Параметр для управления распределением итераций распараллеливаемого цикла между потоками (п. 7.3.1). Директивы: for |
copyin (list) |
Параметр для инициализации постоянных переменных потоков (п. 7.7.4). Директивы: parallel |
copyprivate (list) |
Копирование локальных переменных потоков после выполнения блока директивы single (п. 7.7.1). Директивы: single |
num_treads |
Параметр для задания количества создаваемых потоков в параллельной области (п. 7.7.5). Директивы: parallel |
Приведем для наглядности сводную таблицу использования параметров в директивах
| Параметр | Директива | |||||
|---|---|---|---|---|---|---|
parallel |
for |
sections |
single |
parallel
for |
parallel
sections |
|
if |
( | ( | ( | |||
private |
( | ( | ( | ( | ( | ( |
shared |
( | ( | ( | ( | ||
default |
( | ( | ( | |||
firstprivate |
( | ( | ( | ( | ( | ( |
lastprivate |
( | ( | ( | ( | ||
|
( | ( | ( | ( | ( | |
copyin |
( | ( | ( | |||
schedule |
( | ( | ||||
ordered |
( | ( | ||||
|
( | ( | ( | |||
num_threads |
( | ( | ||||
copyprivate |
( | |||||
Для справки приведем перечень функций библиотеки
| Функция | Описание |
|---|---|
void omp_set_num_threads (int num_threads) |
Установить количество создаваемых потоков (п. 7.7.5) |
int omp_get_max_threads (void) |
Получение максимально-возможного количества потоков (п. 7.7.5) |
int omp_get_num_threads (void) |
Получение количества потоков в параллельной области программы (п. 7.7.5) |
int omp_get_thread_num (void) |
Получение номера потока (п. 7.7.5) |
int omp_get_num_procs (void) |
Получение числа |
void omp_set_dynamic (int dynamic) |
Установить режим динамического создания потоков (п. 7.7.6) |
int omp_get_dynamic (void) |
Получение состояние динамического режима (п. 7.7.6) |
void omp_set_nested (int |
Установить режим поддержки вложенных параллельных фрагментов (п. 7.7.7) |
int omp_get_nested (void) |
Получения состояние режима поддержки вложенных параллельных фрагментов (п. 7.7.7) |
void omp_init_lock (omp_lock_t *lock) void omp_init_nest_lock(omp_nest_lock_t *lock) |
Инициализировать замок (п. 7.5.3) |
void omp_set_lock (omp_lock_t lock) void omp_set_nest_lock (omp_nest_lock_t lock) |
Установить замок (п. 7.5.3) |
void omp_unset_lock (omp_lock_t lock) void omp_unset_nest_lock (omp_nest_lock_t lock) |
Освободить замок (п. 7.5.3) |
int omp_test_lock (omp_lock_t lock) int omp_test_nest_lock (omp_nest_lock_t lock) |
Установить замок без блокировки (п. 7.5.3) |
void omp_destroy_lock(omp_lock_t lock) void omp_destroy_nest_lock(omp_nest_lock_t lock) |
Перевод замка в неинициализированное состояние (п. 7.5.3) |
double omp_get_wtime (void) |
Получение времени текущего момента выполнения программы (п. 7.2.5) |
double omp_get_wtick (void) |
Получение времени в секундах между двумя последовательными показателями времени аппаратного таймера (п. 7.2.5) |
int omp_in_parallel (void) |
Проверка нахождения программы в параллельном фрагменте |
Для справки приведем перечень переменных окружения
| Переменная | Описание |
|---|---|
OMP_SHEDULE |
Переменная для задания способа управления распределением итераций распараллеливаемого цикла между потоками (п. 7.3.1).
Значение по умолчанию: static |
OMP_NUM_THREADS |
Переменная для задания количество потоков в параллельном фрагменте (п. 7.7.5).
Значение по умолчанию: количество . |
OMP_DYNAMIC |
Переменная для задания динамического режима создания потоков (п. 7.7.6).
Значение по умолчанию: false. |
OMP_NESTED |
Переменная для задания режима вложенности параллельных фрагментов (п. 7.7.7).
Значение по умолчанию: false. |
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.