Меню Закрыть

V ИСПОЛЬЗОВАНИЕ ТЕХНОЛОГИИ OPEN МР ПРИ РАЗРАБОТКЕ ПРОГРАММ

image_pdfimage_print

Использование технологии OPEN MP при разработке программ. Обзор директив OPEN MP. Директива parallel. Переменные и их области действия. Синхронизация потоков.

Интерфейс OPEN МР задуман как стандарт программирования для однородных многопроцессорных систем с разделяемой памятью. Примером такой системы является вычислительная система с многоядерным процессором.

Разработкой стандарта занимается организация OPEN МР ARB (ARchitecture Board), в которую вошли представители крупнейших компаний-разработчиков SMP (Symmetric Multi Processor) архитектур и программного обеспечения. Спецификации для языков Fortran и C/C+ + появились соответственно в октябре 1997 года и октябре 1998 года. Сайт www.OPENMP.org – открыт список рассылки для публичного обсуждения OPEN МР (omp@OPEN MP.org). Является высокоуровневой надстройкой над механизмом использования потоков. Последняя версия 3.0 появилась в

мае 2008 года. Документация по этой версии [http://www.OPEN MP.org /mp-documents/spec30.pdf] определяет использование этой технологии для языков Фортран, С, C++. В пособии рассматривается именно эта версия. Так как практическая реализация всех приведенных примеров выполнена с помощью VS2008, в которой реализованы не все возможности последней версии, об этом будет оговорено дополнительно. Так, в этом документе рассмотрена технология OPEN МР только для С, C++.

Необходимо также заметить, что данная технология является Кросс-платформенной, она реализована как в UNIX подобных системах, так и в современных Windows системах, начиная с суперкомпьютеров и кончая desktop.

Использование технологии OPEN МР при разработке программ.

Что такое OPEN МР

Принцип использования состоит в том, что в программу добавляются специальные директивы. Эти директивы игнорируются, если компилятор не поддерживает работу с OPEN МР или не задан необходимый ключ, и обрабатываются, если поддерживает и ключ задан. Директива #pragma языка C+ + обрабатывается именно так, поэтому она используется для этих целей.

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

Таким образом, в соответствии с директивами, в программу добавляются необходимые операторы для создания и уничтожения потоков, а также для их синхронизации. Функции для работы с потоками зависят от используемой платформы. Но так как преобразование кода выполняет дополнительный модуль транслятора, исходный код программы не зависит от платформы, т.е. одинаков для Windows и Unix приложений. Если поддержка директив не включена, то программа не преобразуется, приложение работает в последовательном режиме. Возможность иметь одно и то же приложение для последовательного и параллельного приложения является большим достоинством для случая, когда для обоих режимов используется один и тот же алгоритм, что, к сожалению, бывает редко.

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

Из сказанного выше следует, что каждый поток выполняет одну и ту же потоковую функцию. А если необходимо использовать разные функции для потоков? В этом случае есть 2 решения.

Решение 1. Номер текущего потока всегда можно определить.

Используя эти номера, можно построить потоковую функцию таким образом:

if (текущий номер потока == 0)
{
Код для потока 0
};
else
if (текущий номер потока == 1)
{
Код для потока 1
};

Решение 2. Для задания параллельной области использовать специальные директивы, которые показывают, что в этой области надо выполнять разный код.

Внутри одного из параллельных потоков могут быть вложены средства для распараллеливания. В этом случае данный поток является master-потоком для новых потоков.

Пример псевдокода с использованием директив OPEN МР:

main () {
// Исполняется один поток (первичный или master-поток)
#pragma оmp parallel // Начало параллельного участка программы
{
//Задаются параллельные участки программы
#pragma оmр sections//Начало всех секций
{
#pragma оmр section//1 секция
{...} //Потоковая функция для 1 потока
#pragma оmр section // //2 секция
{...} // Потоковая функция для 2 потока
}
//Ждем завершения всех параллельных секций
... // Продолжение повторяемого кода
#pragma оmр for nowait
// Каждая ветвь цикла выполняется параллельно (в отдельном
потоке)
// Ожидания завершения не требуется
for(...)
{
}
//CS
#pragma оmр critical
//Начало критической секции
{
... // Повторяемый код с эксклюзивным доступом
}
... // Продолжение повторяемого кода
#pragma оmр barrier
//Ждать завершения работы для всех элементов
... //Продолжение повторяемого кода
} // Конец повторяемого кода
// Продолжение последовательного кода
... // Возможно применение новых параллельных конструкторов
}
// Конец последовательного кода

Включение режима поддержки OPEN МР для VS2008

Для включения этого режима необходимо:

  1. Создать проект. Проект может быть любого типа, в том числе консольный.
  2. Добавить к проекту C+ + файл.
  3. В поле Свойств: Project → Properties → <Имя проекта> Property→ Configuration Properties → C/C++ → Language → OPEN MP выбрать Yеs.

Обращаем Ваше внимание, что если Вы забыли включить режим поддержки OPEN МР, то программы, которые используют возможности этого режима при трансляции, не будут выдавать ошибок, а просто будут работать в последовательном режиме, поэтому настоятельно рекомендуем проверять успешность включения этого режима!

Проверка успешности подключения режима поддержки OPEN МР для VS2008

Если режим включен, то компилятор определяет макрос OPEN МР, который содержит версию библиотеки OPEN МР вформате ууууmm (целое данное, старшие 4 цифры которого задают год, а младшие месяц). Таким образом, если макрос OPEN MP определен, то можно сделать вывод об успешности включения режима поддержки OPEN МР, а значение этой переменной фактически определяет версию OPEN МР.

Код для проверки успешности включения и определения версии OPEN МР:

#ifdef _OPENMP
_tprintf (TEXT(«OPEN MP is Support.»
« Version:Year %d, Month = %d\n»),
_OPENMP/100, _OPENMP%100);
#else
printf («_OPEN MP is not defined.\n»);
#endif

Для VS2008 Year = 2002, Month = 3. Вот почему эта реализация не полностью поддерживает возможности 3-й версии OPEN МР от 2008 года.

Функции для определения времени

Для оценки производительности различных вариантов функций необходимо уметь определять временные характеристики. Для измерения времени библиотека OPEN МР содержит функцию omp_get_wtime (), которая, аналогично функции time возвращает время в секундах, прошедшее с определенного момента времени, но в отличие от time, возвращает не целое число секунд, а данное типа double. Фактически разность времен получается с той же точностью, что и точность измерения времени с помощью функции QueryPerformanceCounter. Для определения длительности одного такта (в секундах) используется функция omp_get_wtick().

Заголовки функций:

double omp_get_wtime(void);

double omp_get_wtick(void);

Для использования функций библиотеки OPEN МР необходимо подключить заголовочный файл omp.h.

Обзор директив OPEN МР

Классификация директив

Задание участка кода, который должен выполняться параллельно, управление этим параллельным участком выполняется с помощью директив, которые транслируются до основной трансляции кода.

Директивы делятся на директивы определения параллельных участков и директивы синхронизации.

Общий вид директив

Общий вид директивы #pragma оmр имя директивы [Дополнительные параметры].

Дополнительные параметры зависят от конкретной директивы. Дополнительные параметры не обязательны, задают дополнительную информацию для директивы. Если они задаются, то отделяются друг от друга запятыми. Если при задании директивы опустить ключевое слово оmр, то директива просто игнорируется, и вместо параллельного исполнения получим код, который будет выполняться последовательно. Если директива не помещается в одной строке, для ее продолжения используется символ \ (как для макроса #define).

Директивы для определения параллельных участков. Общая характеристика

Включают в себя директивы:

parallel; for; sections; section.

Директива parallel используется для участка программы, который должен быть выполнен многократно: столько раз, сколько создано потоков. Задается в начале параллельного участка независимо от задания остальных директив.

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

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

Директивы определения класса памяти переменных

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

Директивы синхронизации. Общая характеристика

Позволяют обеспечить атомарное выполнение простейших операций (директива atomic), использование критических секций (директива critical), выполнение отдельного кода только одним потоком (директивы master и single), корректное использование Кешей (директива flush), упорядочивание выполнения кодов в отдельных потоках (ordered). Наряду с директивами можно использовать функции библиотеки OPEN МР для синхронизации потоков.

Директива parallel

Определяет начало и конец параллельного участка программы.

Общий вид директивы

#pragma оmр parallel [Параметры]

После этой директивы задается блок операторов, который выполняется столько раз, сколько потоков соответствует параллельной области.

В качестве параметров могут быть заданы следующие параметры:

  • if (Константное выражение);
  • num_threads (Константное выражение);
  • default (shared | none);
  • private (Список переменных);
  • firstprivate (Список переменных);
  • shared (Список переменных);
  • copyin (Список переменных);
  • reduction (Операция: Список переменных).

Общий вид фрагмента программы с параллельным участком:

//Участок программы до параллельной секции
#pragma оmр parallel
{
Код для параллельного выполнения
}
//Участок программы после параллельной секции

Трансляция параллельной секции

Для этого фрагмента программы для Win32 может быть сформирован код:

//Потоковая функция
DWORD WINAPI ThreadFun (PVOID Par)
{
Код для параллельного выполнения
}
//Участок программы до параллельной секции
//Определение числа потоков (nThreads)
//Создание потоков
HANDLE *h = new HANDLE [nThreads];
for (int i = 0; i &amp;lt; nThreads; ++i)
{
h [i] = CreateThread (0,0, ThreadFun, 0, 0, 0);
}
// Ожидание завершения потоков
WaitForMultipleObject (nThreads, h, TRUE, INFINITE);
//Закрытие потоков и освобождение памяти
for (int i = 0; i &amp;lt; nThreads; ++i)
CloseHandle (h [i]);
delete [ ] h;
//Участок программы после параллельной секции

Включение-выключение параллельного выполнения. Параметр if

Как мы уже знаем, для выключения параллельного выполнения достаточно в поле Properties → C/C++ → Language проекта выключить режим поддержки параллельного выполнения.

Недостаток этого способа: параллельное выполнение отключается для всей программы. А если необходимо отключить только для участка программы? Для этого в качестве дополнительной информации используется условное отключение режима параллельного выполнения:

if (целочисленное выражение).

Если значение выражения равно Истине (не равно 0), директива обеспечивает параллельное выполнение, если Лжи (равно 0) – параллельного выполнения нет. В предыдущем примере достаточно задать

#pragma оmр parallel if (0)

и параллельное выполнение, задаваемое данной директивой, выключается. Если поменять if (0) на if (1), то включаем параллельное выполнение потоков.

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

Если параметр if не задан, то предполагается if (1).

Способы определения числа потоков

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

Количество потоков следует изменять для исследования возможности применения данной программы для разного числа потоков. Изменение числа потоков. Для изменения числа потоков используются следующие способы: параметр num_threads, функции библиотеки OPEN МР и переменные среды.

Использование параметра num_threads

Общий вид параметра num_threads (выражение целого типа). Значение выражения определяет количество потоков для данной параллельной секции.

Использование функций библиотеки OPEN МР

Для использования функций библиотеки необходимо подключить заголовочный файл omp.h.

Функции для определения и установки числа потоков определены в табл. 4

Таблица 4

Функции для управления числом параллельных потоков

Имя

Входные данные Выходные данные Комментарий
omp_set_

num_treads

int Нет Устанавливает число потоков.

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

параметр num_threads

omp_get_

num_threads

Нет int Число потоков
omp_get_

max_threads

 

Нет int Максимальное число потоков. Если число потоков задано функцией omp_set_num_threads, то число этих потоков. Если не задано – то число логических процессоров
omp_get_

thread_num

Нет int Номер текущего потока

 

omp_set_

dynamic

int a Нет if (а) число потоков определяется операционной системой (равно числу логических процессоров);

else – определяется другими способами

 

omp_get_

thread_limit

Нет int

Максимально допустимое число потоков

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

Если необходимо переопределить число потоков независимо от задания динамического режима, следует использовать такой код:

if (omp_get_dynamic())
omp_set_dynamic(0);
omp_set_num_threads(2);
Использование переменных среды OMP_NUM_THREAD и OMP_DYNAMIC для определения числа потоков

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

Для установки переменной используем Му computer→ Properties → Advanced → Environmement Variables → System variables и задаем переменную со своим числовым значением. Если задано отрицательное значение или значение превышает максимально возможное значение потоков ОС, то реакция зависит от реализации. Если задано допустимое число потоков, то оно используется как значение по умолчанию.

Переменная среды OMPDYNAMIC может принимать значения 1 или 0. Установка этого значения эквивалентна вызовам функций omp_set_dynamic(1) и omp_set_dynamic(0) соответственно.

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

Алгоритм определения числа потоков.

if(omp_set_dynamic(l) задана || OMP_DYNAMIC==1)
число потоков равно числу ядер;
else
{
if (параметр num_threads (п))
число потоков равно n;
else
{
if (omp_set_num_threads (m))
число потоков равно m;
else
{
if (OMP_NUM_THREADS == k)
число потоков равно k;
else число потоков равно= числу ядер;
}
}
}

Алгоритм определения максимального числа потоков.

if (omp_set_num_threads (m))
максимальное число потоков равно m;
else максимальное число потоков равно числу ядер;

Распараллеливание цикла

Чтобы сравнить методы распараллеливания циклов с помощью потоков Windows рассмотрим сначала использование потоков Windows, потом использование директив OPEN МР для параллельного выполнения тела цикла. При изложении материала предполагается, что распараллеливается цикл типа for с целочисленным параметром цикла и число итераций кратно шагу изменения параметра цикла.

Также предполагается, что все итерации цикла можно выполнять параллельно, т.е. они независимы.

Пусть необходимо параллельно выполнить цикл с заголовком

for (i = vl; i < v2; i += Step).

Число повторений цикла nFor для такого заголовка определяется по формуле:

\[nFor =(v2-v1)/ Step.\]

Пусть количество потоков, которые будут использоваться для выполнения тела цикла для разных итераций, известно заранее и равно nTthreads. Предположим, что мы хотим равномерно распределить нагрузку между потоками. Если предположить, что каждая итерация цикла выполняется примерно одинаковое время, то каждому потоку необходимо выполнить ⌐nFor / nTthreads ¬ итераций (Запись в скобках ⌐¬ означает округление в большую сторону).

Распараллеливание цикла с помощью потоков Windows

Потоковая функция для каждого потока должна выполнить тело цикла для итераций в заданном диапазоне параметра цикла (Start, Finish). Значения Start, Finish могут быть вычислены в зависимости от номера потока / по формулам:

\[Start = w1 + i*Step;

Finish = Start + Step;\]

Таким образом, потоковой функции надо в списке параметров передать диапазон изменения параметра цикла.

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

typedef struct
{
int Start, Finish, Step;
SrcType src;
DestType dest;
} DATAS, *PDATAS;

Определим потоковую функцию. Для определенности предположим, что внутри цикла необходимо вычислить \(х [i] = i * i\);

// Число итераций цикла
#define&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; VI&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 0
#define&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; V2&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 8192
#define&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Step&amp;nbsp; 1
//Число потоков
#define nTthreads&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2
typedef struct
{
int Start, Finish, Step;
int x[V2];
}DATAS, *PDATAS;
DWORD WINAPI ThreadFun (PVOID Par)
{
PDATAS pDatas = (PDATAS) Par;
INTStart&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; = pDatas -&amp;gt;Start;
INT Finish&amp;nbsp; = pDatas -&amp;gt;Finish;
INT Step = pDatas-&amp;gt;Step;
for (int i = Start; i &amp;lt; Finish; i+=Step)
{
//Тело цикла
pDatas -&amp;gt;x [i] - i * i;
}
_tprintf (_T(«Start = %d Finish = %d Step = %d\n», Start, Finish,
Step);
return 0;
}

Вывод данных внутри потоковой функции выполняется для демонстрации переключения между потоками.

Определим главную программу для создания потоков и выполнения циклов:

int_tmain(int argc, _TCHAR* argv[ ])
{
FIANDLE hThread [nTthreads];
DATAS Datas [nTthreads];
int i = 0, j;
// Число итераций
int nFor = (V2 – VI)/Step;
//Число итераций на один поток
int Н = (nFor + nTthreads – 1)/nTthreads;
for (i = 0; i &amp;lt; nTthreads; i++)
{
Datas [i].Start =V1 +i*H;
Datas [i].Finish = Datas [i].Start + H;
Datas [i].Step = H;
hThread [i] = CreateThread
(0, 0, ThreadFun, &amp;amp;Datas [i], 0,0);
}
WaitForMultipleObjects (nTthreads, hThread, true,
INFINITE);
return 0;
}

Распараллеливание цикла с помощью OPEN МР

В OPEN МР для распараллеливания цикла используется директива Upragma оmр for, за которой должен непосредственно следовать цикл для параллельного выполнения. Общий вид директивы for:

#рragmа оmр for [Дополнительная информация].

Общий вид цикла с параллельным выполнением:

# pragma оmр parallel [Параметры для parallel]
#pragma оmр for [Параметры для for]
for (....)
{
}

Если параллельная секция начинается с цикла, то директивы parallel и for следуют друг за другом, можно использовать сокращенную форму определения параллельных циклов:

#pragma оmр parallel for [Дополнительная информация]
for(....)
{
}

В качестве параметров для директивы for используются параметры:

  • reduction (Операция: Список переменных)
  • private (Список переменных)
  • firstprivate (Список переменных)
  • lastprivate (Список переменных)
  • schedule (Способ[, Размер])
  • collapse (n)
  • ordered
  • nowait

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

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

Вложение параллельных секций

Внутри параллельных секций могут быть другие параллельные секции. Частным случаем вложения параллельных секций является параллельное выполнение вложенных циклов.

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

Для определения режима вложенности используется функция omp_get_nested. Для разрешения вложенности используется функция omp_set_nested (1). Если в качестве параметра задано 0 или эта функция перед использованием вложения не вызывается, распараллеливание не выполняется.

С помощью функции:

int omp_get_max_active_levels ()

можно определить число уровней, установленное по умолчанию или предыдущим вызовом функции omp_set_max_active_levels. Вместо функции для установки числа уровней вложенности можно использовать переменную среды ОМР_МАХ_ACTIVE_LEVELS.

Для циклов в версии 3 с помощью параметра collapse можно определить уровень вложенности циклов, для которых определяется число итераций, например, для кода

#pragma оmр parallel for collapse (2)
for (int i = 0; i &amp;lt; n; ++i)
for (intj = 0;j &amp;lt; m; ++j)

общее число итераций, которые участвуют в распараллеливании, равно n * m. А если параметр collapse отсутствует, то в распараллеливании участвуют только итерации внешнего цикла.

 Особенности использования секций

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

Способ 1.

Выполнять разный код в зависимости от потока, который его выполняет. Напоминаем, что все потоки имеют номера, начиная с 0. В этом случае программа имеет вид:

int Thread = omp_get_num_thread ();
if (Thread ==0)
fun0 (...);
else
if (Thread == 1)
fun1 (...);

Недостатки:

  • если предыдущая часть программы выполняется параллельно, то некоторые из потоков могут быть заняты, а значит, часть функций не будут выполнены;
  • если число функций больше, чем число потоков, то этот метод использовать нельзя.

Способ 2.

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

Недостаток — годится только для параллельного выполнения функций с одинаковым заголовком.

Способ 3.

Использовать секции. Каждая секция фактически определяет потоковую функцию, т.е. то, что будет выполняться параллельно.

Директива sections

Определяет начало задания всех параллельных секций.

Общий вид директивы:

#pragma оmр sections [Параметры]
{
[#pragma оmр section]
{
Секция 1
}
]
[#pragma оmр section
{
Секция 2
}
]
}

Если в параллельной области находится только один параметр sections, то можно использовать сокращенную запись, в которой совмещены директивы parallel и sections, т.е. директива #pragma оmр parallel sections задает начало определения секций.

После определения директивы sections может быть задана одна или более секций. Секция 1, Секция 2,… будут выполняться параллельно.

В качестве дополнительной информации могут задаваться параметры: private, firstprivate, lastprivate, reduction, nowail.

Выполнение master-потока после завершения кода из sections возобновляется только после завершения работы всех внутренних секций (WaitForMultipleObjects (…, TRUE, INFINITE)).

Рекомендации по использованию директивы sections

Таким образом, секции рекомендуется использовать, если необходимо распараллелить отдельные фрагменты кода. Число потоков обычно выбирается равным числу ядер, или число фрагментов кода, которые надо выполнить, параллельно.

Переменные и их область действия. Классы памяти

Классы переменных в C++ программах

Переменная называется внешней, если она описана вне функций. Эта переменная доступна во всех функциях, которые заданы после описания такой переменной. Может быть доступна для функций, которые определены до описания этой переменной и даже в других файлах, если есть определение extern для этой переменной. Память для внешней переменной должна быть выделена только один раз. Директива extern для переменной может быть определена сколько угодно раз. Обычно эта директива определяется в заголовочном файле, который подключается ко всем файлам, где к этой переменной необходимо обращаться. По умолчанию внешние переменные инициализируются 0.

Статические переменные. Определяются вне функций или внутри них. Если статическая переменная определена внутри функций, она видима только внутри этой функции, но после выхода из функции память, выделенная для нее, а значит и ее текущее значение, сохраняется. Если статическая переменная объявлена вне функций, она видима для всех функций, которые определены ниже. Статическая переменная не может быть видимой в других файлах, поэтому в нескольких файлах могут быть объявлены статические переменные с одинаковыми именами, но это будут разные переменные. Статические переменные по умолчанию инициализируются значением 0. Задаются таким образом: static <имя типа> имя переменной.

Локальные переменные. Память выделяется внутри функции. Область действия переменной — блок, где она объявлена. Память автоматически освобождается после завершения функции. Начальное значение не определено.

Классы переменных для OPEN МР.

Классы private и shared

Все переменные делятся на 2 класса: private и shared.

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

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

обеспечена автоматически, класс этих переменных специально определять нельзя.

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

  1. Если переменная задана вне параллельной области, она считается переменной общего доступа {shared). Исключение – параметр цикла, для которого создаются свои копии.
  2. Если память под переменную выделена динамически, то сама память типа shared, а указатель на нее – заданного типа.
  3. Статические данные считаются типа shared.
  4. Переменные, объявленные с квалификатором const, считаются типа shared.
  5. Если переменная объявлена внутри параллельной области, она считается личной переменной потока {private).

Исходя из этих правил, переменными общего доступа являются не только внешние и статические переменные языка C++, но и локальные переменные, которые объявлены до начала параллельной области.

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

shared (Переменная[,Переменная,…]).

private (Переменная[,Переменная,…]).

Классы переменных для OPEN МР.

Параметры firstprivate и lastprivate

Класс firstprivate то же, что класс private, но только переменной при входе в блок задается начальное значение такое же, как для master-потока. После выхода из потока в master-поток значение этой переменной равно значению, присвоенному в master-потоке.

Пример кода с классом переменной firstprivate

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

int a = 10;
#pragma оmр parallel firstprivate(a) num_threads (4)
{
_tprintf (_T(«thread = %d\ta = %d\n»),
omp_get_thread_num (), a);
}

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

Класс lastprivate задает список переменных, которые по завершению параллельной секции становятся такими, какими они были бы при последовательном выполнении. Обычно используется для параметра цикла, значение которого используется за пределами цикла. Перед использованием значение должно быть инициализировано, так как при создании не инициализируется. Одновременно для переменной могут быть заданы классы firstprivat и lastprivate. В этом случае на входе значение совпадает с начальным значением, на выходе – с конечным (как для переменных типа shared), но зато нет проблем совместного доступа, т.е. не требуется синхронизация.

Классы переменных для OPEN МР. Параметр default

Для переменных можно определить их класс по умолчанию. Для этого параметр класса памяти задается так: default (shared\none). Определение shared означает, что если класс памяти для переменной явно не определен, он считается shared. Определение попе означает, что для всех переменных класс должен быть задан явно. Заметим, что последнее определение является наиболее безопасным, так как заставляет программиста «вручную» выбрать тип каждой переменной.

Особенности использования директивы threadprivate и параметра copyin

С помощью параметра private можно задать личную переменную для заданной параллельной области. Для задания личных переменных для группы параллельных областей используется директива threadprivate:

#pragma оmр threadprivate (Список);

Эта директива должна быть задана до первого использования переменных из заданного списка. Обычно эта директива задается для глобальных переменных и может быть общей для нескольких файлов (extern …). Если это локальные переменные, то им надо задать начальное значение до определения параллельной области. В этом случае для всех переменных списка для параллельной области, которая будет задана ниже, будет выделена память под эти переменные и туда будет записано начальное значение, которое было задано при инициализации переменных (своя копия данных). Необходимо остерегаться изменения значения этих переменных до первого использования, особенно через ссылки, так как в этом случае результат зависит от реализации.

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

Если в список переменных типа threadprivate входит класс, для которого необходимо вызывать деструктор, то надо обеспечить завершение всех потоков до завершения параллельной секции. Это связано с тем, что перед завершением параллельной секции необходимо уничтожить все экземпляры переменной типа threadprivate, созданные для этой параллельной области. Для этого в конце параллельной области задается директива barrier.

Ограничения на список переменных при определении их классов.

Рекомендации по использованию

Переменная в выражениях private, firstprivate, lastprivate не должна иметь ссылочный тип (т.е. быть указателем или ссылкой).

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

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

Предыдущий обзор типов переменных показывает, насколько важно правильно выбрать класс переменной. Для того чтобы программист не забывал устанавливать классы всех используемых переменных, что уменьшит вероятность ошибки, необходимо задать параметр default(none). В этом случае компилятор отмечает как ошибочные операторы использования переменных, для которых класс не задан.

Синхронизация потоков

Напоминаем, что синхронизировать необходимо доступ к общим ресурсам (файлы, память). В дополнении к обычным потокам необходимо синхронизировать доступ ко всем данным типа shared. Защищаться необходимо от одновременного выполнения операций записи с чтением или записью (race condition) или взаимных блокировок {deadlock). Для некоторых задач необходимо обеспечить определенный порядок чтения – записи (Производитель – Потребитель).

С помощью OPEN МР создаются потоки одного процесса, что накладывает ограничения на необходимые средства синхронизации.

Средства синхронизации потоков одного процесса для Windows

Для защиты от одновременного доступа к разделяемым ресурсам используются :

  • Interlocked … функции для защиты простых переменных в случае выполнения простейших операций;
  • Критические секции для обеспечения выполнения заданного участка кода одновременно только одним потоком (эксклюзивное выполнение).

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

Достижение необходимого порядка выполнения потоков обеспечивается за счет использования объектов ядра, например событий, семафоров.

Обзор средств синхронизации OPEN МР.

Директива barrier

Общий вид директивы:

#pragma оmр barrier

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

Обзор средств синхронизации OPEN МР.

Директива atomic

Директива atomic обеспечивает атомарное изменение значения переменной типа shared и используется аналогично функциям Interlocked…

Общий вид директивы:

#pragma оmр atomic

Оператор

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

Допускаются операторы вида:

  • х <Бинарная операция> = ехрг
  • x++
  • ++x
  • х—
  • —x

Здесь <Бинарная операция> – операция +, – *, /, &, |, ^, «, ».

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

Заметим, что вариант atomic является наиболее эффективным вариантом для синхронизации доступа к общим данным.

Обзор средств синхронизации OPEN МР.

Критические секции

Рассмотрим создание критических секций с помощью директив.

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

#pragma оmр critical [(<имя_критической_секции>)]

После этой директивы задается блок операторов, входящих в критическую секцию. Заметим, что инициализации и удаления критической секции не требуется. Фактически начало блока соответствует функции EnterCriticalSection, а конец – функции LeaveCriticalSection.

Достоинство критических секций – самый эффективный метод защиты после atomic.

Недостатки критических секций:

  • один поток не может многократно войти в критическую секцию без предварительного ее закрытия даже в одном потоке, поэтому использование в рекурсивной функции критических секций недопустимо;
  • нет возможности проверить состояние критической секции, а это бывает полезным для выполнения других работ на время занятости критической секции;
  • если критическая секция используется в функции и защищает данные, которые передаются как параметр, невозможно проверить, передаются одинаковые параметры или нет (защита имеет смысл только если параметры одинаковы).
  • нельзя из критической секции выйти, используя break, continue, так как тогда критическая секция нормально не завершится.

Обзор средств синхронизации OPEN МР. «Замки»

«Замки» аналогичны критическим секциям, но используют функции библиотеки вместо директив.

Для использования «замка» необходимо:

  1. Инициализировать «замок» до первого использования (аналог – функция InitializeCriticalSection).
  2. Закрыть «замок» в начале критической секции (аналог – функция EnterCriticalSection).
  3. Открыть «замок» в конце критической секции (аналог – функция LeaveCriticalSection).
  4. Деинициализировать «замок» (аналог – функция Delete-CriticalSection).

«Замок» имеет имя, поэтому можно использовать несколько «замков».

Типы данных для «замка»

Для задания «замка» используется тип данных omp_lock_t, который определен в файле omp.h:

typedefvoid * omp_lock_t;

Что означает указатель на данное произвольного типа? В области памяти, выделенной системой для этого данного, хранится состояние «замка» по аналогии с тем, как в структуре CRITICAL­_SECTION состояние критической секции. В отличие от CRITICAL_SECTION, этот «замок» можно закрыть только в том случае, если он открыт. Напоминаем, что для критической секции можно использовать множество функций EnterCriticalSection потоком, который первоначально захватил эту секцию. В структуре CRITICAL_SECTION хранится счетчик использования. В случае многократного захвата критической секции необходимо многократно ее освободить. Критическая секция считается свободной, если счетчик использования равен 0.

Аналогично CRITICAL_SECTION действует «замок» оmр_nest_lock_t, который позволяет повторно входить в критическую секцию потоком, захватившим этот «замок». При этом содержимое счетчика использования увеличивается на 1. Счетчик использования уменьшается функцией omp_unset_nest_lock. «Замок» открывается, если счетчик использования «замка» равен 0.

Тип данных для вложенного «замка»:

typedefvoid * omp_nest_lock_t;

Инициализация «замков»

Для инициализации «замка» используются функции:

void omp_init_lock(omp_lock_t *lock);

void omp_init_nest_lock (omp_nest_lock_t);

Обычно «замок» объявляется таким образом, чтобы он был доступен всем параллельным секциям кода, в которых должен использоваться. При инициализации задается начальное состояние «замка», которое соответствует состоянию «Открыт». Любая параллельная секция может закрыть этот «замок» для использования другими параллельными секциями до того, как секция, которая «замок» закрыла, не откроет его.

Закрытие «замков»

Для закрытия «замка» используются функции:

void omp_set_lock(omp_lock_t *lock);

void omp_ set_nest_lock (omp_nest_lock_t);

Эти функции определяют начало критической секции. Если «замок» открыт, он закрывается, и переходим на выполнение очередного оператора программы. Если «замок» закрыт, поток блокируется до момента открытия «замка». Если использовать эту функцию для неинициализированного «замка» (функции omp_init_lock, omp_init_nest_lock), то формируется исключение во время выполнения программы.

Открытие «замков»

Для открытия «замков» используются функции:

void omp_unset_lock (omp_lock_t *lock);

void omp_unset_nest_lock (omp_nest_lock_t);

Эти функции определяют конец критической секции. Если это обычный «замок», он переходит в состояние Открыт. Если это вложенный «замок», то в состояние Открыт он переходит, если счетчик использования равен 0. Если есть потоки, которые ждут его открытия, выбирается один из них, и переходим на выполнение очередного оператора этого потока. Открыть «замок» может только тот поток, который его закрыл! В противном случае поведение программы зависит от реализации. Так же, как и для предыдущих функций, эти функции можно использовать только для инициализированных соответствующим образом «замков».

Уничтожение «замка»

Используется, если нет потоков, ожидающих его открытия, и не будет больше потоков, которые будут использовать этот «замок».

void omp_destroy_lock(omp_lock_t *lock);

void omp_destroy_nest_lock (omp_nest_lock_t);

Уничтоженными могут быть только инициализированные «замки».

Проверка состояния «замка» (аналог – функция TryEnterCriticalSection)

int оmр_test_lock(omp_lock_t *lock)

int omp_test_nest_lock(omp_nest_lock_t *lock);

Первая функция возвращает ненулевое значение, если «замок» открыт, и 0 в противном случае. Вторая функция возвращает счетчик использования, если «замок» можно использовать, и 0 в противном случае. Если функция возвратила значение TRUE, то выполняется вход в критическую секцию с закрытием «замка».

Обеспечение автоматического открытия «замка» после завершения критической секции

Будем использовать механизм конструктора и деструктора для обеспечения механизма автоматического вызова деструктора:

#pragma once
#include &amp;lt;omp.h&amp;gt;
class omp_lock {
private:
omp_lock_t *lock;
public:
omp_lock (omp_lock_t &amp;amp;lock)
{
this-&amp;gt;lock = lock;
omp_set_lock (this-&amp;gt;lock);
}
~omр_ lock ()
{
omp_unset_lock (lock);
}
};

Использование «замка» в функции:

omp_lock_t lock;
omр_ init_lock(&amp;amp; lock);
#pragma omp parallel
{
{
omp_lock Lock (lock);
//Критическая секция
}
}
omp_destroy_lock(&amp;amp;lock);

Сравнение критических секций, создаваемых с помощью директив и функций

Достоинства директив.

Использование директив проще, чем «замков». Для иллюстрации этого рассмотрите функцию Critical I (), которая выполняет те же действия, что и функция Lockl. В этой функции закомментированы строки, связанные с использованием «замка»:

void Critical1 ()
{
//omp_init_lock(&amp;amp;simple_lock);
#pragma omp parallel num_threads(2)
{
int tid = omp_get_thread_num();
int i;
for (i = 0; i &amp;lt; 5; ++i) {
//omp_set_lock(&amp;amp;simple_lock);
#pragma omp critical
{
_tprintf_s(_TEXT(«Поток %d – начал критический
участок\n»), tid);
_tprintf_s(_TEXT(«Поток %d - кончил критический
участок\n»), tid);
//omp_unset_lock(&amp;amp;simple_lock);
}
}
}
//omp_destroy_lock(&amp;amp;simple_lock);
}

Недостатки директив:

  • директивы используются только для создания критических секций, которые соответствуют обычным (не вложенным) «замкам». Нет средств анализа текущего состояния критической секции;
  • с помощью «замков» можно обеспечить автоматическое открытие «замка» после завершения области его действия.

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

Директивы master и single

Директивы используются, чтобы заданный после директивы блок операторов выполнялся только одним потоком. Если используется директива master, то только master-потоком, если single – то только текущим потоком. Это используется, например, для инициализации и вывода данных, если они одинаковы для всех потоков.

В качестве дополнительной информации для директивы single могут быть заданы параметры:

private (список)

firstprivate (список)

copyprivate (список)

nowait

Здесь используется новый параметр copyprivate (список). Рассмотрим его использование. Пусть в директиве single инициализируются значения приватных переменных. Как сделать, чтобы эти значения были доступны всем одноименным приватным переменным (т.е. переменным, объявленным как private и firstprivate) других параллельных потоков после их инициализации? Для этого эти переменные надо объявить как copyprivate (список). Переменные, заданные в параметре copyprivate, не должны быть в списках private и firstprivate этой директивы.

Для директивы single используется в неявном виде барьер, т.е. все потоки ждут выполнения этого потока. Для того чтобы потоки не ждали выполнения заданного потока, необходимо использовать nowait.

Выводы по средствам синхронизации, предоставляемым OPEN МР

Средства синхронизации обеспечивают:

  • атомарный доступ к простой переменной для выполнения простых одиночных операций;
  • использование критических секций, как без учета счетчика использования, так и с таким счетчиком;
  • автоматически установленные барьеры для ожидания завершения всех параллельных ветвей и возможность снятия этих барьеров;
  • возможность выполнения заданной ветви произвольным потоком (только одним) или master потоком;
  • потребность в выполнении других операций синхронизации.

OPEN МР и обработка исключений

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

Если необходимо обработать исключение, возникшее в произвольном потоке, в master-потоке, то можно использовать такой алгоритм:

  1. В каждом потоке обработать исключение (например, увеличить глобальную переменную, которая определяет число ошибок).
  2. В master-потоке, если есть ошибки, то возбудить исключение.
  3. В master-потоке обработать свое исключение.

Рекомендации по использованию технологии OPEN МР

Технология OPEN МР используется для параллельного выполнения циклов и функций. Позволяет создавать один вариант программы для последовательного и параллельного вариантов программы, а также просто изменять число потоков как для отдельных участков программы, так и для программы в целом.

Есть средства синхронизации потоков программы, аналогичные критическим секциям.

Чего нельзя сделать с помощью технологии OPEN МР? Нельзя параллельно выполнять циклы для типов, отличных от типа for. Для цикла типа for существенные ограничения на тип параметра цикла и выражения для его вычисления и изменения. Синхронизация возможна только в пределах одной программы, так как технология не позволяет использовать объекты ядра.

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

  1. Антонов, А.С. Параллельное программирование с использованием технологии OpenMP: Учебное пособие / А.С. Антонов. – М. : Изд-во МГУ, 2009. – 77 с.
  2. Качко, Е.Г. Параллельное программирование: Учебное пособие / Е.Г. Качко. –
    Харьков:Изд-во «Форт», 2011. – 528 с.

Связанные записи