A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Cancel Create
ProcessCalculus / OpenMP.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cannot retrieve contributors at this time
453 lines (319 sloc) 21.9 KB
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents Copy raw contents
Copy raw contents
OpenMP (Open Multi-Processing) — открытый стандарт для распараллеливания программ на языках C, C++ и Фортран.
Даёт описание совокупности директив компилятора, библиотечных процедур и переменных окружения, которые предназначены для программирования многопоточных приложений на многопроцессорных системах с общей памятью.
Параллельное программирование: OpenMP
- Разработка стандарта ведётся некоммерческой организацией OpenMP Architecture Review Board (ARB), в которую входят все основные производители процессоров, а также ряд суперкомпьютерных лабораторий и университетов
- Поддерживается многими современными компиляторами (входит в стандартную поставку). [https://www.openmp.org/resources/openmp-compilers-tools/]
- Сильно упрощает создание потоков и распределение между ними вычислений для уже написанного кода.
- SPMD (Single Program Multiple Data) подход, подвид MIMD в классификации Флинна
- Последняя версия (на апрель 2023) — 5.2.
- Входит в дистрибутив большинства компиляторов C, С++, Fortran;
Вычисления распараллеливаются между несколькими потоками, включая основной (master) поток:
Это fork-join подход к организации параллелизма — вилочный параллелизм. Где for — создание новых потоков, join — ожидание завершения потоков.
При этом потоки могут быть логически вложенными друг в друга.
OpenMP состоит из трёх компонентов:
- директивы компилятора ( #pragma omp . )
- переменные среды окружения (можно задавать в командной строке)
например. windows: set OMP_NUM_THREADS = 8
linux: export OMP_NUM_THREADS = 8 - вспомогательные (библиотечные) функции; описаны в omp.h
Логические части OpenMP
Структура директивы
#pragma omp [опция1, опция2]
Такая директива влияет на следующей за ней оператор или блок операторов.
#include omp.h> int main()< // последовательный код #pragma omp parallel < // (1) // параллельный код > // (2) // последовательный код return 0;>
- создание функции на основе блока кода
- создание нескольких потоков
- запуск потоков с функцией выполнения
- Ожидание завершения всех потоков.
g++ main.cpp -fopenmp -o myprog
- -fopenmp — флаг: подключить OpenMP и другие нужные OpenMP библиотеки (*.lib или .a файлы). -o myprog — имя исполняемого файла — myprog
Включить поддержку OpenMP в IDE
Вводная — Параллельные(многопоточные) вычисления в OpenMP и C++17.
- Qt Creator. в pro файл добавить: QMAKE_LFLAGS += -fopenmp
- Vusial Studio. Свойства проекта > СС++ > Все параметры > поддержка OpenMP
В секкцим параллельного блока можно вызвать функцию получающаую номер потока. В зависимости от номера потока выполнять ту или иную работу.
#include omp.h> int main()< // последовательный код #pragma omp parallel < // получение номера потока int i = omp_get_thread_num(); // . > // последовательный код return 0;>
Общие и локальные переменные для потоков
Все переменные, созданные до директивы parallel являются общими для всех потоков. Переменные, созданные внутри потока являются локальными (приватными) и доступны только текущему потоку.
Общие переменные при этом не потокобезопасны:
int value = 123; # #pragma omp parallel
Директивы компилятора для || вычислений
Для распараллеливания вычислений:
- parallel Defines a parallel region, which is code that will be executed by multiple threads in parallel.
- for Causes the work done in a for loop inside a parallel region to be divided among threads.
- sections Identifies code sections to be divided among all threads.
- single Lets you specify that a section of code should be executed on a single thread, not necessarily the master thread.
#pragma omp parallel
Требования к || участку кода:
- один вход, один выход. Условные переходы изв || участка кода в последовательный запрещены.
- внутри || участка кода может быть вызвана функция
Явное задание числа потоков
. num_thread( 12 )
Задать число потоков можно с помощью:
- переменной среды окрыжения OMP_NUM_THREADS . Это значение имеет низший приоритет.
- функции omp_set_num_threads() . Средний приоритет.
- директивы num_thread ( ) . Высший приоритет, но только для последующей || области
Разрешить вложенный параллелизм :
- переменная среды окружения: OMP_NESTED = true
- функция omp_set_nested( true )
- условный параллелизм
#pragma omp parallel if ( x > 5)
Количество разделов section внутри section*s* определяет количество параллельных потоков.
#pragma omp parallel sections < #pragma omp section < printf («id = %d, n», omp_get_thread_num()); > #pragma omp section < printf («id = %d, n», omp_get_thread_num()); > >
Задаёт участок кода, который будет выполнятся только одним потоком (не известно каким). single имеет смысл приводить только внутри блока кода parallel
В конце single блока используется неявная барьерная синхронизация — все потоки будут ждать завершения самого медленного потока:
поток 1 =======———| поток 2 ===============| поток 3 ===========—-| выполнение: === ожидание: —
- copyprivate( список переменных ) После завершения, скопирует значения переменных в одноимённые частные переменные родительской параллельной области. Полезно использовать в середине || области, где встречаются вычисления, которые нельзя распараллелить.
- nowait — отменяет барьерную синзронизацию для данной директивы (single, for, parallel)
Выполнится главным потоком. В отличии от single нет будет неявной синхронизации потоков.
Разделеное и совместное использование перемеренных
- Общие переменные — как глобальные переменные, доступны всем потокам. Все переменные, объявленные вне || области считаются общими. Исключения — счётчики циклов.
#pragma omp parallel shared(A, B, C)
Не защищены от неопределённости параллелизма.
- Локальные переменные — у каждого потока своя копия переменной. Даже если переменная объявлена в общей области видимости. Начальное значение переменной не определено. Все переменные объявленные в || области считаются локальными.
int A = -1, b; #pragma omp parallel private(A, b) < A = rand(); printf(«A = %dn», A); // будет выведено несколько (по числу потоков) значений > printf(«A = %dn», A); // будет выведено -1
см. также firstprivate , lastprivate ,
reduction (операция : список переменных)
- создаёт инициализированные копии глобальной переменной в каждом потоке
- в конце || секции выполняется указанная операция с локальными копиями, результат записывается в исходные глобальные переменные
Директивы компилятора для синхронизации
For master and synchronization:
- master Specifies that only the master thread should execute a section of the program.
- critical Specifies that code is only executed on one thread at a time.
- barrier Synchronizes all threads in a team; all threads pause at the barrier, until all threads execute the barrier.
- atomic Specifies that a memory location that will be updated atomically.
- flush Specifies that all threads have the same view of memory for all shared objects.
- ordered Specifies that code under a parallelized for loop should be executed like a sequential loop.
For data environment:
- threadprivate Specifies that a variable is private to a thread.
Векторизация — вид распараллеливания программы, при котором однопоточные приложения, выполняющие одну операцию в каждый момент времени, модифицируются для выполнения нескольких однотипных операций одновременно.
for (i=0; i
Если оператор сложения имеет перегруженную векторную форму, то код можно переписать так:
c = a + b;
Однако, чаще всего такое сложение массивов можно векторизовать частными сложениями отдельных блоков массивов (например по 4 элемента).
- современные компиляторы (с включёнными опциями оптимизации) могут поддерживать автоматическую векторизация.
- автоматическая векторизация может происходить и во время выполнения программы
Векторные операции в процессоре
- набор инструкций SSE процессоров intel
- набор инструкций 3DNow! в процессорах AMD
Процессоры поддерживающие векторные операции (все современный процессоры) хранят данные в векторных регистрах.
g++ main.cpp -o main-novec | 3536 ms |
g++ main.cpp -o main-vec -O3 | 1597 ms |
Компиляция с отчётом о векторизации циклов:
g++ main.cpp -o main-vec -O3 -fopt-info-vec
Вывод:
main.cpp:21:27: optimized: loop vectorized using 16 byte vectors
Комнады компиляция приведены для linux, С++ компилятора GCC. В Windows имя исполняемого файла аналогичного компилятора отличается (см. MinGW)
Флаги коппилятора, связанные с векторизацией:
- -O -fopt-info — показать все подробности об оптимизации. не покажет ничего, если оптимизация не производилась
- -fopt-info-vec — показать информации о векторизации циклов
- -fopt-info-vec-missed — показать не векторизованные операции
- -fopt-info-vec-note — Detailed info about all loops and optimizations being done.
- -fopt-info-vec-all — All previous options together.
Опции оптимизации C++ компилятора GCC:
Оптимизация кода в Microsoft Visual C++: Свойства проекта > раздел СС++ > оптимизация
Передача опций компилятору (GCC) в Qt Creator: в pro-файле проекта добавить: QMAKE_CFLAGS += -O3
pragma ivdep, vector always
#pragma ivdep
ivdep – ignore vector dependencies. Изменяет способ принятия решения о возможности векторизации данного цикла: компилятор по-прежнему анализирует все возможные зависимости между данными, но недоказанные зависимости не принимаются во внимание (считаются несуществующими). Таким образом, если существование зависимости доказано, то векторизация произведена не будет, но все недоказанные зависимости более не будут препятствием для векторизации.
Пример рискованной, с точки зрения компилятора векторизации цикла:
void sum(float *a, float b*, float *c, float *d, float *e, unsigned n)
В ряде случаев компилятор может счесть, что векторизация возможна, но для данного цикла неэффективна. Если же программист считает, что векторизация все равно будет эффективна, можно дать соответствующую рекомендацию компилятору при помощи #pragma vector always . Часто #pragma ivdep и #pragma vector always используются вместе. Важно отметить, что #pragma vector always является лишь рекомендацией и в случае обнаружения зависимости или наличия других веских причин векторизация все равно произведена не будет.
#pragma simd Дает компилятору явное указание произвести векторизацию данного цикла
pragma omp simd
- то же самое, что и #pragma simd , но включённое в стандарт OpenMP
Опции
- safelen( целая константа ) — задаёт максимальное расстояние (в итерациях) между итерациями, которые могут выполняться параллельно
. #pragma omp simd safelen(8) for (unsigned i=8; i8] >
- simdlen ( число ) — предпочтительное число || выполняющихся итераций
- alignment (список, целое число)
- linear ( переменная : положительное число)
Ограничения Если в цикле вызывается функция, то в общем случае этот цикл не векторизуется. Чтобы избежать этого, отдельные функция можно сделать векторизуемыми
#pragma omp declare simd float sum(float a, float b)return a+b;> #pragma omp simd for (unsigned i=0; i
Один поток генерирует задачи, другие — выполняют, подход похожий на пулл потоков
Задача может выполнятся немедленно или спустя некоторое время после её назначения потоку. Выполнение задачи может быть прервано, а потом продолжено.
- shared
- private
- firstprivate
- default
- if
- untied. Если задача была начата одним потоком, то может быть продолжена любым другим потоком. По умолчанию, отложенная задача выполняется тем же потоком, что начинал её выполнение.
Пример
Обход списка с выполнением длительных операций над каждым элементом
#pragma omp parallel < // выдавать задачи должен выдавать один поток. он же будет обходить список // nowait — отключить барьерную синхронизацию #pragma omp single nowait < node = list_head; while (node)< // создание задачи, которую будет выполнять другие потоки // firstprivate — создание локальной копии переменной в каждом отдельном потоке, выполняющем каждую отдельную задачу. // локальная (private) для потока переменная будет инициализирована (forst) значением глобальной переменной node #pragma omp task firstprivate(node) < independent_work( node ); > node = node->next; > > >
- В чём разница между parallel и task?
- В чём разница между section и task?
Больше примеров опций векторизации:
#pragma omp traget
#pragma omptarget map(to:b,c,d) map(from:a) // перед циклом переместит переменные b,c,d на отдельное устройство, // после цикла переместит переменную a с устройства // < #pragma omp parallel for for (i=0; i >
- Parallel STL
- Intel TBB
- https://www.openmp.org//
- https://www.coursera.org/learn/parallelnoye-programmirovaniye/home/welcome
- Документация (MSVC): https://learn.microsoft.com/ru-ru/cpp/parallel/openmp/openmp-in-visual-cpp?view=msvc-170
- Пример: https://github.com/ivtipm/ProcessCalculus/blob/master/examples/openMP_1/main.cpp
Источник: github.com
Урок 24: многопоточность — OpenMP
Для C++ существует API позволяющее очень красиво распараллелить вычисления на многоядерную архитектуру процессора — OpenMP.
Это не единственная модель вычислений, часто удобно просто явным образом запустить отдельный поток вычислений, или явно работать с защелкой (lock), или использовать другие примитивы. Но сегодня мы обсудим один из самых популярных и часто используемых вариантов в случае когда речь идет о C++ и обработке картинок — распараллеливание обработки в цикле.
P.S. не забудьте компилировать это задание в Release.
Распараллеливание цикла
Представим что есть цикл который увеличивает каждый элемент вектора в два раза:
std::vectorint> xs = . ; for (int i = 0; i xs.size(); ++i) xs[i] *= 2; >
Вот такое указание скажет компилятору что мы хотим выполнить цикл в несколько потоков чтобы использовать все ядра (и виртуальные потоки) процессора.
#pragma omp parallel for // omp = OpenMP, parallel = запустить потоки, for = распределить по ним индексы этого цикла for (int i = 0; i xs.size(); ++i) xs[i] *= 2; >
Иначе говоря каждый индекс i для которого этот цикл должен быть выполнен — достанется для выполнения одному из потоков.
Критическая секция
Что если мы хотим суммировать все числа массива? В чем проблема такого, казалось бы, естественного решения?
long long totalSum = 0; #pragma omp parallel for // parallel = запустить потоки, for = распределить по ним индексы этого цикла for (int i = 0; i xs.size(); ++i) totalSum += xs[i]; >
В том что здесь происходит состояние гонки при считывании и записи текущего значения totalSum . Ведь мы одновременно делает это из разных потоков.
Как это можно исправить? Правильно, воспользовавшись lock -ом (защелкой, по сути замком на двери уборной), который будет гарантировать нам что лишь один из потоков сейчас работает с переменной totalSum .
long long totalSum = 0; // используем long long (64-битное число) чтобы при суммировании int — не переполнился результат #pragma omp parallel for for (int i = 0; i xs.size(); ++i) #pragma omp critical // omp = OpenMP, critical = это критическая секция кода, в нее потоки заходят по одному totalSum += xs[i]; > >
Какова будет производительность (скорость работы) такого кода? Почему?
Как бы это сделать лучше?
Reduction
Очень часто хочется делать подобные операции как в прошлой функции — объединение результата между разными потоками.
Т.е. по сути хочется применить некую операцию между результатами работы всех потоков, такое преобразование называется редукция (reduction).
Причем операции бывают разные — например суммирование (или перемножение, взятие максимума, взятие минимума).
Если мы укажем что потоки должны провести редукцию над totalSum , то каждый поток сделает свою личную копию этой переменной, будет суммировать свои элементы в эту копию (по сути накапливая свою частичную сумму), и когда весь цикл будет обработан — все потоки соберутся, покажут друг другу накопленное значение этой переменной, и объединив результаты — запишут в оригинальную переменную totalSum общий результат.
long long totalSum = 0; #pragma omp parallel for reduction(+: totalSum) // в скобочках идут параметры редукции — сначала идет операция, затем — название переменной-аккумулятора for (int i = 0; i xs.size(); ++i) totalSum += xs[i]; >
Насколько быстро работает такой код относительно однопоточной версии и версии с critical секцией? Почему?
Какие еще операции ложаться на эту парадигму? Какое свойство от них требуется? Ложиться ли например на эту идею операция вычитания? А взятие наибольшего общего делителя?
Самописный Reduction
Что нужно чтобы суметь написать редукцию самостоятельно (просто из любопытства и чтобы научиться писать более сложные вещи)?
1) Запустить каждый поток
2) В каждом потоке завести аккумулятор для частичной суммы (его личная копилка)
3) Пройтись по циклу и в каждом потоке добавлять элементы в свою переменную
4) Просуммировать все частичные суммы в общую переменную
long long totalSum = 0; int threadsN = 0; #pragma omp parallel // ЗДЕСЬ НЕТ for, но есть parallel = говорим что эту секцию хочется запустить для каждого потока процессора int threadId = -1; #pragma omp critical // в критической секции рассчитаемся по номерам потоков и выведем в консоль что такой-то поток был запущен threadId = threadsN; std::cout <«Thread #» <threadId <» started. » <std::endl; ++threadsN; > long long threadSum = 0; #pragma omp for // ЗДЕСЬ НЕТ parallel, т.к. это ключевое слово говорит «запускай потоки», но потоки уже запущены, for (int i = 0; i xs.size(); ++i) // осталось лишь распределить среди них вычислительную рабочую нагрузку threadSum += xs[i]; // почему здесь не нужна критическая секция? > #pragma omp critical totalSum += threadSum; std::cout <«Thread #» <threadId <» finished!» <std::endl; > >
А зачем здесь первая критическая секция? Нельзя ли ее сделать без синхронизации? Попробуйте.
А зачем здесь вторая критическая секция? Нельзя ли ее сделать без синхронизации? Попробуйте.
Как распределяются вычисления?
Как выяснить какие индексы цикла достаются потоку при вычислении? Попробуйте придумать эксперимент который позволит это выяснить опытным путем.
Источник: www.polarnick.com
Параллельное программирование с использованием OpenMP
При использовании многопроцессорных вычислительных систем с общей памятью обычно предполагается, что имеющиеся в составе системы процессоры обладают равной производительностью, являются равноправными при доступе к общей памяти, и время доступа к памяти является одинаковым (при одновременном доступе нескольких процессоров к одному и тому же элементу памяти очередность и синхронизация доступа обеспечивается на аппаратном уровне). Многопроцессорные системы подобного типа обычно именуются симметричными мультипроцессорами ( symmetric multiprocessors, SMP ).
Перечисленному выше набору предположений удовлетворяют также активно развиваемые в последнее время многоядерные процессоры, в которых каждое ядро представляет практически независимо функционирующее вычислительное устройство. Для общности излагаемого учебного материала для упоминания одновременно и мультипроцессоров и многоядерных процессоров для обозначения одного вычислительного устройства (одноядерного процессора или одного процессорного ядра ) будет использоваться понятие вычислительного элемента ( ВЭ ).
Рис. 7.1. Архитектура многопроцессорных систем с общей (разделяемой) с однородным доступом памятью (для примера каждый процессор имеет два вычислительных ядра)
Следует отметить, что общий доступ к данным может быть обеспечен и при физически распределенной памяти (при этом, естественно, длительность доступа уже не будет одинаковой для всех элементов памяти). Такой подход именуется как неоднородный доступ к памяти ( non-uniform memory access or NUMA ).
В самом общем виде системы с общей памятью могут быть представлены в виде модели параллельного компьютера с произвольным доступом к памяти ( parallel random-access machine — PRAM ) см., например, Andrews (2000).
Обычный подход при организации вычислений для многопроцессорных вычислительных систем с общей памятью — создание новых параллельных методов на основе обычных последовательных программ, в которых или автоматически компилятором, или непосредственно программистом выделяются участки независимых друг от друга вычислений. Возможности автоматического анализа программ для порождения параллельных вычислений достаточно ограничены, и второй подход является преобладающим. При этом для разработки параллельных программ могут применяться как новые алгоритмические языки, ориентированные на параллельное программирование , так и уже имеющиеся языки программирования, расширенные некоторым набором операторов для параллельных вычислений.
Широко используемый подход состоит и в применении тех или иных библиотек, обеспечивающих определенный программный интерфейс ( application programming interface, API ) для разработки параллельных программ. В рамках такого подхода наиболее известны Windows Thread API (см., например, Вильямс (2001)) и PThead API (см., например, Butenhof (1997)). Однако первый способ применим только для ОС семейства Microsoft Windows , а второй вариант API является достаточно трудоемким для использования и имеет низкоуровневый характер.
Все перечисленные выше подходы приводят к необходимости существенной переработки существующего программного обеспечения, и это в значительной степени затрудняет широкое распространение параллельных вычислений. Как результат, в последнее время активно развивается еще один подход к разработке параллельных программ, когда указания программиста по организации параллельных вычислений добавляются в программу при помощи тех или иных внеязыковых средств языка программирования — например, в виде директив или комментариев, которые обрабатываются специальным препроцессором до начала компиляции программы. При этом исходный текст программы остается неизменным, и по нему, в случае отсутствия препроцессора, компилятор построит исходный последовательный программный код. Препроцессор же, будучи примененным, заменяет директивы параллелизма на некоторый дополнительный программный код (как правило, в виде обращений к процедурам какой-либо параллельной библиотеки).
Рассмотренный выше подход является основой технологии OpenMP (см., например, Chandra et al. (2000)), наиболее широко применяемой в настоящее время для организации параллельных вычислений на многопроцессорных системах с общей памятью. В рамках данной технологии директивы параллелизма используются для выделения в программе параллельных фрагментов, в которых последовательный исполняемый код может быть разделен на несколько раздельных командных потоков ( threads ). Далее эти потоки могут исполняться на разных процессорах ( процессорных ядрах ) вычислительной системы. В результате такого подхода программа представляется в виде набора последовательных ( однопотоковых ) и параллельных ( многопотоковых ) участков программного кода (см. рис. 7.2). Подобный принцип организации параллелизма получил наименование » вилочного » ( fork-join ) или пульсирующего параллелизма. Более полная информация по технологии OpenMP может быть получена в литературе (см., например, Chandra et al. (2000), Roosta (2000)) или в информационных ресурсах сети Интернет .
Рис. 7.2. Общая схема выполнения параллельной программы при использовании технологии OpenMP
При разработке технологии OpenMP был учтен накопленный опыт по разработке параллельных программ для систем с общей памятью. Опираясь на стандарт X3Y5 (см. Chandra et al. (2000)) и учитывая возможности PThreads API (см. Butenhof (1997)), в технологии OpenMP в значительной степени упрощена форма записи директив и добавлены новые функциональные возможности. Для привлечения к разработке OpenMP самых опытных специалистов и для стандартизации подхода на самых ранних этапах выполнения работ был сформирован Международный комитет по OpenMP ( the OpenMP Architectural Review Board, ARB ). Первый стандарт, определяющий технологию OpenMP применительно к языку Fortran, был принят в 1997 г., для алгоритмического языка C — в 1998 г. Последняя версия стандарта OpenMP для языков C и Fortran была опубликована в 2005 г. (см. www. openmp . org ).
Далее в разделе будет приведено последовательное описание возможностей технологии OpenMP . Здесь же, еще не приступая к изучению, приведем ряд важных положительных моментов этой технологии:
- Технология OpenMP позволяет в максимальной степени эффективно реализовать возможности многопроцессорных вычислительных систем с общей памятью, обеспечивая использование общих данных для параллельно выполняемых потоков без каких-либо трудоемких межпроцессорных передач сообщений.
- Сложность разработки параллельной программы с использованием технологии OpenMP в значительной степени согласуется со сложностью решаемой задачи — распараллеливание сравнительно простых последовательных программ, как правило, не требует больших усилий (порою достаточно включить в последовательную программу всего лишь несколько директив OpenMP ) 1 Сразу хотим предупредить, чтобы простота применения OpenMP для первых простых программ не должна приводить в заблуждение — при разработке сложных алгоритмов и программ требуется соответствующий уровень усилий и для организации параллельности — см. раздел 12 настоящего учебного курса ; это позволяет, в частности, разрабатывать параллельные программы и прикладным разработчикам, не имеющим большого опыта в параллельном программировании.
- Технология OpenMP обеспечивает возможность поэтапной ( инкрементной ) разработки параллельных программы — директивы OpenMP могут добавляться в последовательную программу постепенно (поэтапно), позволяя уже на ранних этапах разработки получать параллельные программы, готовые к применению; при этом важно отметить, что программный код получаемых последовательного и параллельного вариантов программы является единым и это в значительной степени упрощает проблему сопровождения, развития и совершенствования программ.
- OpenMP позволяет в значительной степени снизить остроту проблемы переносимости параллельных программ между разными компьютерными системами — параллельная программа, разработанная на алгоритмическом языке C или Fortran с использованием технологии OpenMP , как правило, будет работать для разных вычислительных систем с общей памятью.
7.1. Основы технологии OpenMP
Перед началом практического изучения технологии OpenMP рассмотрим ряд основных понятий и определений, являющихся основополагающими для данной технологии.
7.1.1. Понятие параллельной программы
Под параллельной программой в рамках OpenMP понимается программа, для которой в специально указываемых при помощи директив местах — параллельных фрагментах — исполняемый программный код может быть разделен на несколько раздельных командных потоков ( threads ). В общем виде программа представляется в виде набора последовательных ( однопотоковых ) и параллельных ( многопотоковых ) участков программного кода (см. рис. 7.2).
Важно отметить, что разделение вычислений между потоками осуществляется под управлением соответствующих директив OpenMP . Равномерное распределение вычислительной нагрузки — балансировка ( load balancing ) — имеет принципиальное значение для получения максимально возможного ускорения выполнения параллельной программы.
Потоки могут выполняться на разных процессорах ( процессорных ядрах ) либо могут группироваться для исполнения на одном вычислительном элементе (в этом случае их исполнение осуществляется в режиме разделения времени). В предельном случае для выполнения параллельной программы может использоваться один процессор — как правило, такой способ применяется для начальной проверки правильности параллельной программы.
Количество потоков определяется в начале выполнения параллельных фрагментов программы и обычно совпадает с количеством имеющихся вычислительных элементов в системе; изменение количества создаваемых потоков может быть выполнено при помощи целого ряда средств OpenMP . Все потоки в параллельных фрагментах программы последовательно перенумерованы от 0 до np-1 , где np есть общее количество потоков. Номер потока также может быть получен при помощи функции OpenMP .
Использование в технологии OpenMP потоков для организации параллелизма позволяет учесть преимущества многопроцессорных вычислительных систем с общей памятью. Прежде всего, потоки одной и той же параллельной программы выполняются в общем адресном пространстве , что обеспечивает возможность использования общих данных для параллельно выполняемых потоков без каких-либо трудоемких межпроцессорных передач сообщений (в отличие от процессов в технологии MPI для систем с распределенной памятью). И, кроме того, управление потоками (создание, приостановка, активизация, завершение) требует меньше накладных расходов для ОС по сравнению с процессами.
7.1.2. Организация взаимодействия параллельных потоков
Как уже отмечалось ранее, потоки исполняются в общем адресном пространстве параллельной программы. Как результат, взаимодействие параллельных потоков можно организовать через использование общих данных, являющихся доступными для всех потоков. Наиболее простая ситуация состоит в использовании общих данных только для чтения. В случае же, когда общие данные могут изменяться несколькими потоками, необходимы специальные усилия для организации правильного взаимодействия. На самом деле, пусть два потока исполняют один и тот же программный код
для общей переменной n . Тогда в зависимости от условий выполнения данная операция может быть выполнена поочередно (что приведет к получению правильного результата) или же оба потока могут одновременно прочитать значение переменной n , одновременно увеличить и записать в эту переменную новое значение (как результат, будет получено неправильное значение). Подобная ситуация, когда результат вычислений зависит от темпа выполнения потоков, получил наименование гонки потоков ( race conditions ). Для исключения гонки необходимо обеспечить, чтобы изменение значений общих переменных осуществлялось в каждый момент времени только одним единственным потоком — иными словами необходимо обеспечить взаимное исключение ( mutual exclusion ) потоков при работе с общими данными. В OpenMP взаимоисключение может быть организовано при помощи неделимых ( atomic ) операций, механизма кри тических секций ( critical sections ) или специального типа семафоров — замков ( locks ).
Следует отметить, что организация взаимного исключения приводит к уменьшению возможности параллельного выполнения потоков — при одновременном доступе к общим переменным только один из них может продолжить работу, все остальные потоки будут блокированы и будут ожидать освобождения общих данных. Можно сказать, что именно при реализации взаимодействия потоков проявляется искусство параллельного программирования для вычислительных систем с общей памятью — организация взаимного исключения при работе с общими данными является обязательной, но возникающие при этом задержки (блокировки) потоков должны быть минимальными по времени.
Помимо взаимоисключения, при параллельном выполнении программы во многих случаях является необходимым та или иная синхронизация ( synchronization ) вычислений, выполняемых в разных потоках: например, обработка данных, выполняемая в одном потоке, может быть начата только после того, как эти данные будут сформированы в другом потоке (классическая задача параллельного программирования » производитель-потребитель » — «producer- consumer » problem ). В OpenMP синхронизация может быть обеспечена при помощи замков или директивы barrier .
7.1.3. Структура OpenMP
Конструктивно в составе технологии OpenMP можно выделить:
- Директивы,
- Библиотеку функций,
- Набор переменных окружения.
Именно в таком порядке и будут рассмотрены возможности технологии OpenMP .
7.1.4. Формат директив OpenMP
Стандарт предусматривает использование OpenMP для алгоритмических языков C90, C99, C++, Fortran 77, Fortran 90 и Fortran 95. Далее описание формата директив OpenMP и все приводимые примеры программ будут представлены на алгоритмическом языке C; особенности использования технологии OpenMP для алгоритмического языка Fortran будут даны в п. 7.8.1.
В самом общем виде формат директив OpenMP может быть представлен в следующем виде:
#pragma omp [[[,] ]…]
Начальная часть директивы (# pragma omp) является фиксированной, вид директивы определяется ее именем (имя_директивы), каждая директива может сопровождаться произвольным количеством параметров (на английском языке для параметров директивы OpenMP используется термин clause ).
Для иллюстрации приведем пример директивы:
#pragma omp parallel default(shared) private(beta,pi)
Пример показывает также, что для задания директивы может быть использовано несколько строк программы — признаком наличия продолжения является знак обратного слеша «».
Действие директивы распространяется, как правило, на следующий в программе оператор, который может быть, в том числе, и структурированным блоком.
Источник: intuit.ru