Модель ветвление-слияние




Базовые понятия OpenMP

Введение

OpenMP (Open specifications for Multi–Processing) – стандарт для написания параллельных программ для многопроцессорных вычислительных систем с общей оперативной памятью. Программа представляется как набор нитей (threads), объединённых общей памятью, где проблема синхронизации решается введением критических секций и мониторов.

Стандарт OpenMP был разработан в 1997 году, как API, ориентированный на написание портируемых многопоточных приложений. Сначала он был основан на языке Fortran, но позднее включил в себя и C/C++.

Разработкой стандарта занимается организация OpenMP ARB (ARchitecture Board), в которую вошли представители крупнейших компаний – разработчиков SMP-архитектур и программного обеспечения. Спецификации для языков Fortran и C/C++ появились соответственно в октябре 1997 года и октябре 1998 года. OpenMP задуман как стандарт для программирования на масштабируемых SMP-системах (SSMP, ccNUMA, etc.) в модели общей памяти (shared memory model). На данный момент последняя официальная спецификация стандарта – OpenMP 3.1 (принятая в июле 2011 года).

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

За счет идеи “частичного распараллеливания” OpenMP идеально подходит для разработчиков, желающих быстро распараллелить свои вычислительные программы с большими параллельными циклами. Разработчик не создает новую параллельную программу, а просто добавляет в текст последовательной программы OpenMP директивы. Предполагается, что OpenMP-программа на однопроцессорной платформе может быть использована в качестве последовательной программы, т.е. нет необходимости одновременно поддерживать последовательную и параллельную версии. Директивы OpenMP просто игнорируются последовательным компилятором, а для вызова процедур OpenMP могут быть подставлены заглушки (stubs), текст которых приведен в спецификациях.

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

Функции OpenMP служат в основном для изменения и получения параметров окружения. Кроме того, OpenMP включает API-функции для поддержки некоторых типов синхронизации. Чтобы задействовать эти функции OpenMP библиотеки периода выполнения (исполняющей среды), в программу нужно включить заголовочный файл omp.h. Если же используется в приложении только OpenMP-директивы pragma, включать этот файл не требуется.

Модель ветвление-слияние

OpenMP используется модель параллельного выполнения “ветвление-слияние” (fork-join). Программа начинается выполнением одной нити, называемой начальной (initial) нитью. Начальная нить выполняется последовательно. Когда нить достигает директивы parallel она создает команду нитей, состоящую из неё самой и нуля или более дополнительных нитей, и становится хозяйкой (master) созданной команды. Все члены команды исполняют код структурной области, связанной с директивой parallel (параллельной области). В конце параллельной области размещается неявный барьер. Только нить-хозяйка продолжает выполнение после завершения параллельной области.

Число нитей в команде, выполняющихся параллельно, можно контролировать несколькими способами. Один из них – использование переменной окружения OMP_NUM_THREADS. Другой способ – вызов процедуры omp_set_num_threads(). Еще один способ – использование выражения num_threads в сочетании с директивой parallel.

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

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

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

Модель памяти

OpenMP предполагает, что программа выполняется в системе с разделяемой памятью, в которой хранятся (могут быть сохранены и выбраны) переменные программы доступные всем её нитям. Кроме того каждая нить имеет доступ к памяти, недоступной для всех других нитей; такая память называется частной памятью нити.

Директива parallel разделяет память, доступную параллельной области, на разделяемую (shared) и частную (private). Каждая переменная, встречающаяся в параллельной области, имеет соотвествующую одноименную (оригинальную) переменную вне параллельной области. Разделяемая переменная ссылается на ту же память, что и оригинальная переменная вне параллельной области. Для каждой частной переменной создается новая переменная в памяти каждой нити, кроме, быть может, нити-хозяйки. Частные переменные в параллельной области ссылаются на частную память нити.

Дополнительно каждая нить может иметь своё временное представление памяти. Это временное представление не является необходимой частью модели памяти OpenMP, но учитывает наличие в современных вычислительных системах регистров процессора, кэшей различных уровней и других структур, позволяющих нити кэшировать переменные и избегать лишних обращений к памяти. Это временное представление не обязано быть все время согласованным с памятью. Поэтому OpenMP предоставляет средства позволяющие принудительно установить согласованность временного представления с памятью. Предполагается, что компилятор C/C++ поддерживающий OpenMP, автоматически поддерживает согласованность разделяемых переменных, описанных с ключевым словом volatile. Естественно предполагается, что для таких переменных проводится принудительное согласование непосредственно перед чтением и непосредственно после записи.

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

Компиляция

Для использования механизмов OpenMP нужно скомпилировать программу компилятором, поддерживающим OpenMP, с указанием соответствующего ключа например:

icc/ifort используется ключ компилятора -openmp
gcc /gfortran -fopenmp
Sun Studio -xopenmp
Visual C++ - /openmp
PGI -mp

Компилятор интерпретирует директивы OpenMP и создаёт параллельный код. При использовании компиляторов, не поддерживающих OpenMP, директивы OpenMP игнорируются без дополнительных сообщений. Компилятор с поддержкой OpenMP определяет макрос _OPENMP, который может использоваться для условной компиляции отдельных блоков, характерных для параллельной версии программы. При необходимости это определение можно протестировать, как показано ниже:

#ifdef _OPENMP
fn();
#endif

 

 

Директивы OpenMP

Значительная часть функциональности OpenMP реализуется при помощи директив компилятору. Они должны быть явно вставлены пользователем, что позволит выполнять программу в параллельном режиме. В C/C++ директивы OpenMP определяются конструкциями #pragma, предусматривающимися стандартами C и C++, и используемых для задания дополнительных указаний компилятору. Использование специальной ключевой директивы “omp” указывает на то, что команды относятся к OpenMP и для того, чтобы исключить случайные совпадения имён директив OpenMP с другими именами. Таким образом директивы #pragma для работы с OpenMP имеют следующий формат:

#pragma omp директива<> опция[ [ [,] опция]...]

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

Опция (clause) – это необязательный модификатор директивы, влияющий на ее поведение. Списки опций, поддерживаемые каждой директивой, различаются, а пять директив (master, critical, flush, ordered и atomic) вообще не поддерживают опции.

OpenMP поддерживает директивы parallel, for, parallelfor, section, sections, single, master, critical, flush, ordered и atomic, и ряд других, которые определяют механизмы разделения работы или конструкции синхронизации.

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

Опция schedule

Управляет распределением работы между нитями в конструкции распределения работы цикла.

schedule тип([, chunk])

Опция задаёт, каким образом итерации цикла распределяются между нитями; Задается вид алгоритма планирования и, если необходимо, числовой параметр алгоритма (обычно размер блока пространства итераций).

В опции schedule параметр type задаёт следующий тип распределения итераций:

· static – блочно-циклическое распределение итераций цикла; размер блока – chunk. Первый блок из chunk итераций выполняет нулевая нить, второй блок – следующая и т.д. до последней нити, затем распределение снова начинается с нулевой нити. Если значение chunk не указано, то всё множество итераций делится на непрерывные куски примерно одинакового размера (конкретный способ зависит от реализации), и полученные порции итераций распределяются между нитями.

· dynamic – динамическое распределение итераций с фиксированным размером блока: сначала каждая нить получает chunk итераций (по умолчанию chunk=1), та нить, которая заканчивает выполнение своей порции итераций, получает первую свободную порцию из chunk итераций. Освободившиеся нити получают новые порции итераций до тех пор, пока все порции не будут исчерпаны. Последняя порция может содержать меньше итераций, чем все остальные.

· guided – динамическое распределение итераций, при котором размер порции уменьшается с некоторого начального значения до величины chunk (по умолчанию chunk=1) пропорционально количеству ещё не распределённых итераций, делённому на количество нитей, выполняющих цикл. Размер первоначально выделяемого блока зависит от реализации. В ряде случаев такое распределение позволяет аккуратнее разделить работу и сбалансировать загрузку нитей. Количество итераций в последней порции может оказаться меньше значения chunk.

· auto – способ распределения итераций выбирается компилятором и/или системой выполнения. Параметр chunk при этом не задаётся.

· runtime – способ распределения итераций выбирается во время работы программы по значению переменной среды OMP_SCHEDULE. Параметр chunk при этом не задаётся.

Директива parallel

Директива parallel создает параллельную область для следующего за ней структурированного блока, параллельная область задаётся при помощи записи:

#pragma omp parallel опция[[[,] опция]...]структурированный
блок

Возможные опции:

· if (условие) – выполнение параллельной области по условию. Вхождение в параллельную область осуществляется только при выполнении некоторого условия. Если условие не выполнено, то директива не срабатывает и продолжается обработка программы в прежнем режиме;

· num_threads (целочисленное выражение) – явное задание количества нитей, которые будут выполнять параллельную область; по умолчанию выбирается последнее значение, установленное с помощью функции omp_set_num_threads(), или значение переменной OMP_NUM_THREADS;

· default(shared|none) – всем переменным в параллельной области, которым явно не назначен класс, будет назначен класс shared; none означает, что всем переменным в параллельной области класс должен быть назначен явно;

· private (список) – задаёт список переменных, для которых порождается локальная копия в каждой нити; начальное значение локальных копий переменных из списка не определено;

· firstprivate (список) – задаёт список переменных, для которых порождается локальная копия в каждой нити; локальные копии переменных инициализируются значениями этих переменных в нити-мастере;

· shared (список) – задаёт список переменных, общих для всех нитей;

· copyin (список) – задаёт список переменных, объявленных как threadprivate, которые при входе в параллельную область инициализируются значениями соответствующих переменных в нити-мастере;

· reduction (оператор:список) – задаёт оператор и список общих переменных; для каждой переменной создаются локальные копии в каждой нити; локальные копии инициализируются соответственно типу оператора (для аддитивных операций – 0 или его аналоги, для мультипликативных операций – 1 или её аналоги); над локальными копиями переменных после выполнения всех операторов параллельной области выполняется заданный оператор; оператор это:+, *, -, &, |, ^, &&, ||; порядок выполнения операторов не определён, поэтому результат может отличаться от запуска к запуску.

Эта директива сообщает компилятору, что структурированный блок кода должен быть выполнен параллельно, в нескольких потоках. Каждый поток будет выполнять один и тот же поток команд, но не один и тот же набор команд – все зависит от операторов, управляющих логикой программы, таких как if-else.

При входе в параллельную область порождаются новые OMP_NUM_THREADS-1 нитей, каждая нить получает свой уникальный номер, причём порождающая нить получает номер 0 и становится основной нитью группы (“мастером”). Остальные нити получают в качестве номера целые числа с 1 до OMP_NUM_THREADS-1. Количество нитей, выполняющих данную параллельную область, остаётся неизменным до момента выхода из области. При выходе из параллельной области производится неявная синхронизация и уничтожаются все нити, кроме породившей.

Если один из потоков параллельной области встречает другую директиву parallel, то он создает новую группу потоков, согласно правилам, и становится основной нитью новой группы.

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

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

Во время исполнения любой поток может приостоновить выполнение своей неявной задачи в точке планирования задач (task scheduling point) и переключиться на выполнение любой явно-сгенерированной задачи прежде чем возобновить выполнение неявной задачи.

Нить может узнать свой номер с помощью вызова библиотечной функции omp_get_thread_num.

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

Например, если внутри параллельной области содержится только один параллельный цикл или одна конструкция sections, то можно использовать укороченную запись: parallel for или parallel sections. При этом допустимо указание всех опций этих директив, за исключением опции nowait.

Ограничения для директивы parallel следующие:

· Программа не должна зависеть от какого-либо порядка определения опций параллельной директивы, или от каких-либо побочных эффектов определения опций;

· Только одна опция if может присутствовать в директиве;

· Только одна опция num_threads может присутствовать в директиве. Выражение в опции num_threads должно быть целочисленным;

· Бросок исключения выполненный внутри параллельной области должен вызывать обработку исключения в рамках одной параллельной области, и той же нити, которая бросила исключение.

Директива for

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

#pragma omp for опция[[[,] опция]... ]цикл
for

Эта директива относится к идущему следом за данной директивой блоку, включающему оператор for.

Возможные опции:

· private (список) – задаёт список переменных, для которых порождается локальная копия в каждой нити; начальное значение локальных копий переменных из списка не определено;

· firstprivate (список) – задаёт список переменных, для которых порождается локальная копия в каждой нити; локальные копии переменных инициализируются значениями этих переменных в нити-мастере;

· lastprivate (список) – переменным, перечисленным в списке, присваивается результат с последнего витка цикла;

· reduction (оператор:список) – задаёт оператор и список общих переменных; для каждой переменной создаются локальные копии в каждой нити; локальные копии инициализируются соответственно типу оператора (для аддитивных операций – 0 или его аналоги, для мультипликативных операций – 1 или её аналоги); над локальными копиями переменных после завершения всех итераций цикла выполняется заданный оператор; оператор это: +, *, -, &, |, ^, &&, ||; порядок выполнения операторов не определён, поэтому результат может отличаться от запуска к запуску;

· schedule(type[, chunk]) – опция задаёт, каким образом итерации цикла распределяются между нитями;

· collapse(n) – опция указывает, что n последовательных тесновложенных циклов ассоциируется с данной директивой; для циклов образуется общее пространство итераций, которое делится между нитями; если опция collapse не задана, то директива относится только к одному непосредственно следующему за ней циклу;

· ordered – опция, говорящая о том, что в цикле могут встречаться директивы ordered; в этом случае определяется блок внутри тела цикла, который должен выполняться в том порядке, в котором итерации идут в последовательном цикле;

· nowait – в конце параллельного цикла происходит неявная барьерная синхронизация параллельно работающих нитей: их дальнейшее выполнение происходит только тогда, когда все они достигнут данной точки; если в подобной задержке нет необходимости, опция nowait позволяет нитям, уже дошедшим до конца цикла, продолжить выполнение без синхронизации с остальными.

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

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

Следующий пример демонстрирует использование директивы for. В последовательной области инициализируются три исходных массива A, B, C. В параллельной области данные массивы объявлены общими. Вспомогательные переменные i и n объявлены локальными. Каждая нить присвоит переменной n свой порядковый номер. Далее с помощью директивы for определяется цикл, итерации которого будут распределены между существующими нитями. На каждой i-ой итерации данный цикл сложит i-ые элементы массивов A и B и результат запишет в i-ый элемент массива C. Также на каждой итерации будет напечатан номер нити, выполнившей данную итерацию.

#include <stdio.h>
#include <omp.h>

int main(int argc, char *argv[])
{
int A[10], B[10], C[10], i, n;
// Заполним исходные массивы
for (i = 0; i < 10; i++)
{
A[i] = i;
B[i] = 2 * i;
C[i] = 0;
}

#pragma omp parallel shared(A, B, C) private(i, n)
{
// Получим номер текущей нити
n = omp_get_thread_num();

#pragma omp for
for (i = 0; i < 10; i++)
{
C[i] = A[i] + B[i];
printf("Нить \%d сложила элементы с номером %d\n", n, i);
}
}
}

Директива single

Если в параллельной области какой-либо участок кода должен быть выполнен лишь один раз, то его нужно выделить директивой single.

#pragma omp single опция[ [[,] опция]...]структурированный
блок

Возможные опции:

· private (список) – задаёт список переменных, для которых порождается локальная копия в каждой нити; начальное значение локальных копий переменных из списка не определено;

· firstprivate (список) – задаёт список переменных, для которых порождается локальная копия в каждой нити; локальные копии переменных инициализируются значениями этих переменных в нити-мастере;

· copyprivate (список) – после выполнения нити, содержащей конструкцию single, новые значения переменных списка будут доступны всем одноименным частным переменным (private и firstprivate), описанным в начале параллельной области и используемым всеми её нитями; опция не может использоваться совместно с опцией nowait; переменные списка не должны быть перечислены в опциях private и firstprivate данной директивы single;

· nowait – после выполнения выделенного участка происходит неявная барьерная синхронизация параллельно работающих нитей: их дальнейшее выполнение происходит только тогда, когда все они достигнут данной точки; если в подобной задержке нет необходимости, опция nowait позволяет нитям, уже дошедшим до конца участка, продолжить выполнение без синхронизации с остальными.

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

Какая именно нить будет выполнять выделенный участок программы, не специфицируется. Одна нить будет выполнять данный фрагмент, а все остальные нити будут ожидать завершения её работы, если только не указана опция nowait.

Следующий пример иллюстрирует применение опции copyprivate. В данном примере переменная n объявлена в параллельной области как локальная. Каждая нить присвоит переменной n значение, равное своему порядковому номеру, и напечатает данное значение. В области single одна из нитей присвоит переменной n значение 100, и на выходе из области это значение будет присвоено переменной n на всех нитях. В конце параллельной области значение n печатается ещё раз и на всех нитях оно равно 100.

#include <stdio.h>
#include <omp.h>

int main(int argc, char *argv[])
{
int n;

#pragma omp parallel private(n)
{
n = omp_get_thread_num();
printf("Значение n начало(): %d\n", n);

#pragma omp single copyprivate(n)
{
n = 100;
}

printf("Значение n конец(): %d\n", n);
}
}

Ограничения для директивы single следующие:

· Опция copyprivate не должна использоваться вместе с опцией nowait;

· Только одна опция nowait может быть использована в директиве single;

· Исключение брошенное в области single должно быть обработанно в рамках одной области single, одной и той же нитью.

Директива sections

Директива sections используется для задания конечного (неитеративного) параллелизма. Директива sections содержит набор структурированных блоков, которые распределяются по потокам в группе. Каждый структурированный блок исполняется один раз одним из потоков в группе.

#pragma omp sections опция[[[,] опция]...]
{
#pragma omp sectionструктурированный
блок
#pragma omp sectionструктурированный
блок
...
}

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

Возможные опции:

· private (список) – задаёт список переменных, для которых порождается локальная копия в каждой нити; начальное значение локальных копий переменных из списка не определено;

· firstprivate (список) – задаёт список переменных, для которых порождается локальная копия в каждой нити; локальные копии переменных инициализируются значениями этих переменных в нити-мастере;

· lastprivate (список) – переменным, перечисленным в списке, присваивается результат, полученный в последней секции;

· reduction (оператор:список) – задаёт оператор и список общих переменных; для каждой переменной создаются локальные копии в каждой нити; локальные копии инициализируются соответственно типу оператора (для аддитивных операций – 0 или его аналоги, для мультипликативных операций – 1 или её аналоги); над локальными копиями переменных после завершения всех секций выполняется заданный оператор; оператор это: +, *, -, &, |, ^, &&, ||; порядок выполнения операторов не определён, поэтому результат может отличаться от запуска к запуску;

· nowait – в конце блока секций происходит неявная барьерная синхронизация параллельно работающих нитей: их дальнейшее выполнение происходит только тогда, когда все они достигнут данной точки; если в подобной задержке нет необходимости, опция nowait позволяет нитям, уже дошедшим до конца своих секций, продолжить выполнение без синхронизации с остальными.

Директива section задаёт участок кода внутри секции sections для выполнения одной нитью.

#pragma omp section

Перед первым участком кода в блоке sections директива section не обязательна. Какие именно нити будут задействованы для выполнения какой секции, не специфицируется. Если количество нитей больше количества секций, то часть нитей для выполнения данного блока секций не будет задействована. Если количество нитей меньше количества секций, то некоторым (или всем) нитям достанется более одной секции.

Следующий пример демонстрирует использование опции lastprivate. В данном примере опция lastprivate используется вместе с директивой sections. Переменная n объявлена как lastprivate переменная. Три нити, выполняющие секции section, присваивают своей локальной копии n разные значения. По выходе из области sections значение n из последней секции присваивается локальным копиям во всех нитях, поэтому все нити напечатают число 3. Это же значение сохранится для переменной n и в последовательной области.

#include <stdio.h>
#include <omp.h>

int main(int argc, char *argv[])
{
int n = 0;

#pragma omp parallel
{
#pragma omp sections lastprivate(n)
{
#pragma omp section
{
n = 1;
}

#pragma omp section
{
n = 2;
}

#pragma omp section
{
n = 3;
}
}

printf("Значение n на нити %d: %d\n",
omp_get_thread_num(), n);
}

printf("Значение n в последовательной области: %d\n", n);
}

Директива master

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

#pragma omp master

Следующий пример демонстрирует применение директивы master. Переменная n является локальной, то есть каждая нить работает со своим экземпляром. Сначала все нити присвоят переменной n значение 1. Потом нить-мастер присвоит переменной n значение 2, и все нити напечатают значение n. Затем нитьмастер присвоит переменной n значение 3, и снова все нити напечатают значение n. Видно, что директиву master всегда выполняет одна и та же нить. В данном примере все нити выведут значение 1, а нить-мастер сначала выведет значение 2, а потом - значение 3.

#include <stdio.h>

int main(int argc, char *argv[])
{
int n;

#pragma omp parallel private(n)
{
n = 1;

#pragma omp master
{
n = 2;
}

printf("Первое значение n: %d\n", n);

#pragma omp barrier

#pragma omp master
{
n = 3;
}

printf("Второе значение n: %d\n", n);
}
}

Директива critical

С помощью директив critical оформляется критическая секция программы. Критическая секция запрещает одновременное исполнение структурированного блока более чем одним потоком.

#pragma omp critical имя[()]структурированный
блок

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

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

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

#include <stdio.h>
#include <omp.h>
int main(int argc, char *argv[])
{
int n;

#pragma omp parallel
{
#pragma omp critical
{
n = omp_get_thread_num();
printf("Нить %d\n", n);
}
}
}

Если бы в примере не была указана директива critical, результат выполнения программы был бы непредсказуем. С директивой critical порядок вывода результатов может быть произвольным, но это всегда будет набор одних и тех же чисел от 0 до OMP_NUM_THREADS-1. Конечно, подобного же результата можно было бы добиться другими способами, например, объявив переменную n локальной, тогда каждая нить работала бы со своей копией этой переменной. Однако в исполнении этих фрагментов разница существенная.

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

Директива barrier

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

#pragma omp barrier

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

Следующий пример демонстрирует применение директивы barrier. Директива barrier используется для упорядочивания вывода от работающих нитей. Выдачи с разных нитей "Сообщение 1"и "Сообщение 2"могут перемежаться в произвольном порядке, а выдача "Сообщение 3"со всех нитей придёт строго после двух предыдущих выдач.

#include <stdio.h>
#include <omp.h>
int main(int argc, char *argv[])
{
#pragma omp parallel
{
printf("Сообщение 1\n");

printf("Сообщение 2\n");

#pragma omp barrier

printf("Сообщение 3\n");
}
}

Директива atomic

Частым случаем использования критических секций на практике является обновление общих переменных. Например, если переменная sum является общей и оператор вида sum = sum + expr находится в параллельной области программы, то при одновременном выполнении данного оператора несколькими нитями можно получить некорректный результат. Чтобы избежать такой ситуации можно воспользоваться механизмом критических секций или специально предусмотренной для таких случаев директивой atomic.

#pragma omp atomic [ read | write | update | capture ]оператор

или

#pragma omp atomic captureструктурированный
блок

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

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

#include <stdio.h>
#include <omp.h>
int main(int argc, char *argv[])
{
int count = 0;

#pragma omp parallel
{
#pragma omp atomic
count++;
}

printf("Число нитей: %d\n", count);
}

Директива ordered

Директивы ordered определяют блок внутри тела цикла, который должен выполняться в том порядке, в котором итерации идут в последовательном цикле.

#pragma omp orderedструктурированный
блок

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

Следующий пример иллюстрирует применение директивы ordered и опции ordered. Цикл for помечен как ordered. Внутри тела цикла ид



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2018-01-08 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: