Что же такое OpenMP? Разработка этого теперь широко используемого стандарта многопоточного программирования началась почти десять лет назад. Распространенные в то время операционные системы (Windows, Unix, Solaris) предполагали различные методы программирования потоков. Основная задача, которая стояла перед разработчиками стандарта OpenMP, — создать программный интерфейс, позволяющий использовать многопоточный программный код как в ОС Windows, так и в Unix/Linux, а кроме того, совместимый с наиболее распространенными языками программирования.

OpenMP  — это реализация метода мультипоточного программирования (multithreading). Всю OpenMP- программу можно разделить на последовательные и параллельные секции. Для выполнения параллельного участка кода порождается необходимое количество дочерних потоков, причем как именно это происходит в конкретной операционной системе, скрыто от программиста. То есть при разработке программ можно сосредоточиться на алгоритмической части, не тратя сил на реализацию технических аспектов параллелизма.

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

Создание OpenMP-проекта

Итак, откроем новый проект в MS Visual Studio 2005. Это будет пустое консольное приложение для Win32 (Win32 Console Application). Для того чтобы Visual Studio не создавала автоматически никакого программного кода, нужно в мастере Win32 Application Wizard на второй вкладке установить флажок Empty Project (Пустой проект) (рис.1).

Рис 1. Создание проекта в MS Visual Studio 2005

Теперь в папке Source File (Файлы с программным кодом) создадим новый файл, который и будет содержать код нашего приложения (листинг 1).

Дальше необходимо включить в проект поддержку OpenMP, в противном случае компилятор проигнорирует все прагмы OpenMP. Для этого в Solution Explorer надо вызвать меню, щелкнув правой кнопкой мыши по самой первой строчке, содержащей название проекта (в нашем случае OpenMP1), и выбрать пункт Properties в выпадающем меню. В левой части появившегося окна раскройте список с заголовком С/С++, затем выберите в нем Language, и тогда справа появится список опций, одна из которых будет носить имя OpenMP Support (рис.2).

Рис 2. Включение в проект Visual Studio поддержки OpenMP

В выпадающем списке, расположенном напротив этой опции, нужно выбрать Yes (/openmp), тем самым добавив в проект поддержку OpenMP. Теперь можно закрыть окно, нажав кнопку OK. Запустим приложение с помощью комбинации клавиш +.

Рассмотрим более подробно, как работает наша программа. Выражение #pragma omp parallel сообщает компилятору о том, что описывается некая OpenMP-конструкция, а именно параллельная секция. Это подразумевает, что участок кода, стоящий в фигурных скобках, будет выполняться параллельно на нескольких вычислительных ядрах. Заключительная конструкция в рассматриваемой директиве — private (Thread_Id). Здесь необходимо вспомнить, что в системах с общей памятью, для программирования которых и создавался OpenMP, необходимо всегда различать общие и частные переменные (см. врезку «Работа с переменными…»).

Директива private (Thread_Id) указывает, что переменная Thread_Id в создаваемом параллельном регионе будет частной. Нужно это для того, чтобы присвоить ей номер текущего потока, используя функцию omp_get_num_threads (). В то же время функция omp_get_thread_num () вернет некоторое целое число — общее число порожденных потоков. Очевидно, что для любого потока выполнения это число будет одним и тем же, а значит, нет необходимости менять тип переменной Num_Of_Threads с общего на частный, подобно переменной Thread_Id. Стоит добавить, что все функции OpenMP начинаются с приставки «omp_» и все порожденные в рамках определенного параллельного региона потоки нумеруются с нуля.

Параллельные регионы OpenMP

Рассмотрим классический код программы для вычисления полинома (листинг 2).

Вычисление полинома высоких степеней — задача простая, но требующая значительных вычислительных ресурсов. Следует обратить внимание на то, что прог-раммный код в данном случае уже оптимизирован: так, если сумму произведений S вычислять по формуле S = S + a [j] * pow (x, j), где функция pow () возводит аргумент x в степень j на каждом шаге цикла, то падение производительности будет иметь просто катастрофический характер. Поэтому следующий шаг к сокращению времени работы программы — распределение вычислений между всеми доступными процессорами.

Рассмотрим код более подробно. Здесь вычислительная нагрузка сосредоточена внутри цикла. Самый простой способ распараллелить цикл — добавить прагму #pragma omp parallel for, как это сделано в листинге 3.

Здесь приведен фрагмент программы, поясняющий, как именно нужно модифицировать цикл for в функции main. Мы указываем компилятору, что хотим организовать вычисления таким образом, чтобы данный цикл выполнялся параллельно в нескольких потоках. Однако далеко не всегда достаточно добавить такую директиву. Часто требуется, чтобы любые две итерации цикла были независимы одна от другой по данным. То есть если результат вычислений зависит от порядка их выполнения, то имеет место взаимная зависимость итераций между собой и компилятор, определив это, не сможет распараллелить данный цикл. В таких случаях нужно разделить работу между вычислительными потоками другим способом.

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

Прагма omp parallel sections декларирует создание одной или нескольких параллельных секций, однако главная задача этой конструкции — описать, каким образом упомянутые секции будут взаимодействовать. В рассматриваемом примере указывается, что переменная S в порождаемых параллельных секциях будет общей, для чего используется конструкция shared (S). Нужно это для того, чтобы вычисленная сумма была корректной — в обеих описанных ниже секциях переменная S используется для суммирования. Затем директива num_threads (2) сообщает компилятору, что для выполнения кон-струкции parallel sections следут создать два потока. Тут появляется некоторое отличие от реализации параллелизма в предыдущих примерах. Так, при использовании прагмы parallel for вычислительные потоки порождались в момент выполнения программы и их количество определялось количеством доступных процессорных ядер, а сейчас код программы заранее требует, чтобы в параллельном регионе были созданы ровно два потока. Стоит добавить, что директива num_threads (N), где N — целое положительное число, может использоваться и с другими OpenMP-конструкциями, допустим, так: #pragma omp parallel for num_threads (4).

В нашем примере мы создаем две секции кода, которые взаимодействуют посредством сохранения вычисленных значений в общей переменной S. Можно воспользоваться и другим подходом для вычисления суммы. Скажем, объявить еще две переменные S1 и S2, присвоить им нулевые значения до объявления параллельных секций, затем в каждой секции вычислить свою сумму элементов S1 или S2. Таким образом, переменные S1 и S2 будут содержать «полусуммы», после чего результат S можно будет найти как их сумму. Фрагмент кода, реализующий данную идею, приведен в листинге 5.

Важно понимать, что переменные S1 и S2 объявлены вне параллельного региона, а значит, для обеих созданных секций являются общими. В этом кроется потенциальная опасность: так, например, возможно, что в результате ошибки программиста второй поток выполнения запишет какое-либо значение в переменную S1, которую использует для суммирования первый поток. Некорректный результат очевиден. Чтобы этого избежать, целесообразно использовать директиву private (S1,S2). Описанный здесь пример, конечно, несколько надуман, однако по мере увеличения числа строк параллельного кода вероятность возникновения ошибки вследствие некорректного использования общих переменных возрастает.

Синхронизация выполнения параллельных потоков

Синхронизация потоков, выполняющихся в рамках одной программы, наиболее часто требуется, когда планируется совместное использование разделяемых ресурсов (а к таким относятся и общие переменные) или когда потоки начинают обмениваться сообщениями между собой. OpenMP значительно упрощает задачу: проблемы организации совместного доступа к общим переменным в параллельных секциях скрыты от программиста. Так, в рассмотренных выше примерах переменные, описанные в параллельной секции как shared (), с точки зрения прикладного программиста ничем не отличаются от всех остальных. Между тем имеются и дополнительные сред-ства синхронизации:

  1. #pragma omp barrier (создание барьера): выполнение любого потока, достигшего данной прагмы, будет приостановлено до тех пор, пока все порожденные в рамках запущенной программы потоки не достигнут этой точки. В рассмотренных выше случаях в параллельных секциях данная конструкция задействуется по умолчанию.
  2. Директива nowait (используется, например, так: #pragma omp for nowait) по действию обратна барьеру. Скажем, если в рамках параллельного региона потоки будут созданы с данной директивой, то синхронизации их между собой происходить не будет.

В заключение стоит отметить, что наиболее полно стандарт OpenMP описан на странице www. openmp. org, а детали его реализации в MS Visual Studio 2005 рассмотрены в MSDN.


Работа с переменными в многопоточных приложениях

Общая переменная (shared) в вычислительных системах с общей памятью — это именованный участок памяти, доступный всем OpenMP-потокам. В то же время частные (private) — это локализированные переменные, и каждый процесс обладает собственной копией такой переменной. Доступ к ним закрыт для всех создающихся потоков, кроме одного — владельца, поэтому все изменения некоторой частной переменной в одном потоке никак не повлияют на работу остальных. По умолчанию все переменные в OpenMP-программе общие, но с исключениями: частными являются индексы параллельных циклов и переменные, которые объявлены в параллельных регионах. И наконец, для переменной можно указать, что она будет частной в рамках некоторого параллельного региона.