Общие и частные переменные в openmp программа скрытая ошибка
Доброго времени суток, друзья! Как вы могли заметить, я иногда балуюсь с параллельным программированием посредством OpenMP. На данный момент готова заметка о том, как установить и настроить omp в Clion. А из реализаций есть параллельное умножение матриц. Я посчитал, что самое время собрать небольшую шпаргалку по наиболее часто используемым директивам и их параметрам, чтобы можно было сюда заглянуть и освежить их в памяти. Постараюсь на каждую директиву добавить по небольшому, чисто символическому синтаксическому примеру.
Не секрет, что OpenMP доступен на языках C/C++ и Fortran(даже слышал, что и на Java есть), но я буду писать примеры только для C/C++, уж не сердитесь, теория для них все равно одинаковая. Предлагаю ни секунды не терять и приступать к делу.
Общий синтаксис вызова директив OpenMP
Директива parallel
Самая основная директива, служит для создания нитей(threads). Блок, который следует за директивой будет выполнен параллельно. Количество вновь созданных нитей можно установить с помощью вызова функции omp_set_num_threads() передав ей целое число соответствующее желаемому количеству. Узнать номер нити в коде можно вызвав функцию omp_get_thread_num().
Важно! Если написать вложенную директиву parallel, то каждая из созданных нитей, встретив директиву, создаст еще такое же количество дополнительных нитей, будьте внимательны.
Опции директивы parallel
Пример использования
Директива for
Используется для явного распараллеливания следующего цикла for, при этом каждая нить начнет со своего индекса. Если не указывать директиву, то цикл будет пройден каждой нитью полностью от начала и до конца.
Пример использования
Нужно очень внимательно относиться к распараллеливанию циклов в программе, точно знать, что алгоритм не пострадает от того, что не все нити полностью пройдут по циклу.
Директива single
Служит для того, чтобы выполнить некий блок в параллельной области только один раз. Часто используется для выводов, чтобы только одна нить печатала информацию.
Пример использования
При этом какая именно нить выполнит участок кода, заранее мы никак не узнаем.
Директива master
Аналогична директиве single, но мы точно знаем, что блок выполнится мастер-нитью. Мастером является та нить, которая породила все остальные, ее порядковый номер равен 0.
Директива critical
Похожа на две предыдущие. Разница заключается в том, что блок после директивы critical выполнится всеми нитями, но по очереди. Если блок уже исполняется любой нитью, то все остальные блокируются на входе и ждут завершения. После завершения работы, случайным образом из списка заблокированных выбирается новая. Таким образом в один момент блок будет исполняться только одной нитью.
Пример использования
Директива barrier
Классический метод барьерной синхронизации, нити будут блокироваться на этой директиве до тех пор, пока не дождутся всех. Как только все нити достигают этой точки, работа продолжается.
Пример использования
Заключение
Это далеко не самый полный список, признаю, сейчас я записал сюда только самые основные директивы и опции, которые использую, использовал и буду использовать. Поэтому вполне вероятно, что список, описания и примеры будут пополняться. Но на сегодня у меня все, спасибо за внимание!
Функции
Функции OpenMP носят скорее вспомогательный характер, так как реализация параллельности осуществляется за счет использования директив. Однако в ряде случаев они весьма полезны и даже необходимы. Функции можно разделить на три категории: функции исполняющей среды, функции блокировки/синхронизации и функции работы с таймерами. Все эти функции имеют имена, начинающиеся с omp_, и определены в заголовочном файле omp.h. К рассмотрению функций мы вернемся в следующих заметках.
Директивы
Директива parallel
Самой главной можно пожалуй назвать директиву parallel. Она создает параллельный регион для следующего за ней структурированного блока, например:
Директива parallel указывает, что структурный блок кода должен быть выполнен параллельно в несколько потоков. Каждый из созданных потоков выполнит одинаковый код содержащийся в блоке, но не одинаковый набор команд. В разных потоках могут выполняться различные ветви или обрабатываться различные данные, что зависит от таких операторов как if-else или использования директив распределения работы.
Чтобы продемонстрировать запуск нескольких потоков, распечатаем в распараллеливаемом блоке текст:
На 4-х ядерной машине мы можем ожидать увидеть следующей вывод
Но на практике я получил следующий вывод:
Это объясняется совместным использованием одного ресурса из нескольких потоков. В данном случае мы выводим на одну консоль текст в четырех потоках, которые никак не договариваются между собой о последовательности вывода. Здесь мы наблюдаем возникновение состояния гонки (race condition).
Состояние гонки — ошибка проектирования или реализации многозадачной системы, при которой работа системы зависит от того, в каком порядке выполняются части кода. Эта разновидность ошибки являются наиболее распространенной при параллельном программировании и весьма коварна. Воспроизведение и локализация этой ошибки часто бывает затруднена в силу непостоянства своего проявления (смотри также термин Гейзенбаг).
Директива for
Рассмотренный нами выше пример демонстрирует наличие параллельности, но сам по себе он бессмыслен. Теперь извлечем пользу из параллельности. Пусть нам необходимо извлечь корень из каждого элемента массива и поместить результат в другой массив:
Если мы напишем:
Теперь каждый создаваемый поток будет обрабатывать только отданную ему часть массива. Например, если у нас 8000 элементов, то на машине с четырьмя ядрами работа может быть распределена следующим образом. В первом потоке переменная i принимает значения от 0 до 1999. Во втором от 2000 до 3999. В третьем от 4000 до 5999. В четвертом от 6000 до 7999. Теоретически мы получаем ускорение в 4 раза. На практике ускорение будет чуть меньше из-за необходимости создать потоки и дождаться их завершения. В конце параллельного региона выполняется барьерная синхронизация. Иначе говоря, достигнув конца региона, все потоки блокируются до тех пор, пока последний поток не завершит свою работу.
Можно использовать сокращенную запись, комбинируя несколько директив в одну управляющую строку. Приведенный выше код будет эквивалентен:
Директивы private и shared
Относительно параллельных регионов данные могут быть общими (shared) или частными (private). Частные данные принадлежат потоку и могут быть модифицированы только им. Общие данные доступны всем потокам. В рассматриваемом ранее примере массив представлял общие данные. Если переменная объявлена вне параллельного региона, то по умолчанию она считается общей, а если внутри то частной. Предположим, что для вычисления квадратного корня нам необходимо использовать промежуточную переменную value:
Чтобы сделать переменную для каждого потока частной (private) мы можем использовать два способа. Первый — объявить переменную внутри параллельного региона:
Второй — воспользоваться директивой private. Теперь каждый поток будет работать со своей переменной value:
Помимо директивы private, существует директива shared. Но эту директиву обычно не используют, так как и без нее все переменные объявленные вне параллельного региона будут общими. Директиву можно использовать для повышения наглядности кода.
Мы рассмотрели только малую часть директив OpenMP и продолжим знакомство с ними в следующих уроках.
Я исходил из предположения, что если у вас был вложенный цикл, в котором вы хотели использовать OpenMP, вам нужно было сделать переменные цикла для внутренних циклов частными, как показано ниже.
Однако, когда я запускаю тот же цикл без private(y,z) , он все равно работает нормально. Итак, каковы на самом деле ситуации, когда вам нужно использовать private / shared ?
Значение a[x+y+z]=a[x]+a[y]+a[z]; не покажет вам каких-либо проблем, потому что это очень надежное решение в отношении возможных потенциальных ошибок, которые вы ищете.
Попробуйте вычисление, которое уникально для каждого элемента массива, написанного и записывающего каждый элемент массива только один раз.
Например, возьмите меньший массив, размер 1000, цикл 0-9, вычислите индекс x + 10 * y + 100 * y и напишите индекс в значение.
Затем экспериментируйте, чтобы спровоцировать ошибки, например, swap x, y, z в расчете.
Вы заметите, что результат не чист, среди прочего, потому что y и z становятся доступными. Некоторые элементы массива не будут записаны (инициализируйте их, например, 1111, чтобы заметить), а некоторые не будут записывать свой индекс.
Возможно, напишите чекер, подтвердив, что индекс и значение идентичны для каждой записи после этого.
Важно понимать разницу между общим и частным, и вы, кажется, так хорошо себя чувствуете. Следующее, что нужно узнать, это то, что переменные, объявленные за пределами параллельного региона, неявно shared а переменные, объявленные внутри, являются неявно private . Это имеет смысл, если вы подумаете об этом и таким образом можете и должны сохранить явное объявление, всегда объявляя переменные как можно локальнее.
В вашем коде это означает:
Это значительно упрощает анализ вашего кода - с OpenMP и без него. Для OpenMP это означает, что код по умолчанию 1 . ( a неявно shared и x,z,y неявно private )
Явные предложения об обмене данными создают избыточность, а также могут быть опасны, если вы точно не знаете, что вы делаете: частные переменные не инициализируются, даже если они имеют допустимое значение вне параллельного региона и что даже означает использование частной переменной после параллельная область? Поэтому я настоятельно рекомендую использовать неявный подход по умолчанию, если это не требуется для расширенного использования.
1 Ваш код на самом деле не так, потому что несколько потоков записывают разные значения в одно и то же место в памяти - и даже читают их одновременно. Но я не буду подробно объяснять, потому что вы упомянули об этом как пример. Тем не менее, общий доступ, скорее всего, вы хотите для такого цикла.
Библиотека OpenMP часто используется в математических вычислениях, т.к. позволяет очень быстро и без особого труда распараллелить вашу программу. При этом, идеология OpenMP не очень хорошо подойдет, скажем, при разработке серверного ПО (для этого существуют более подходящие инструменты).
Этой библиотеке посвящено множество книг и статей. Наиболее популярными являются книги Антонова [3] и Гергеля [4], мной также был написан ряд статей (поверхностных) по этой теме [5,6], и ряд примеров программ на нашем форуме [7]. На мой взгляд, у этих книг есть ряд недостатков (которые я, конечно, исправляю):
Вычислительные системы. Идеология OpenMP
рис.2 директива omp parallel
Программа описывает переменную value , общую для всех потоков. Каждый поток увеличивает значение переменной, а затем выводит полученное значение на экран. Я запустил ее на двухъядерном компьютере, получил следующие результаты:
Проблема гонки потоков OpenMP
Для ряда операций более эффективно использовать директиву atomic , чем критическую секцию. Она ведет себя также, но работает чуть быстрее. Применять ее можно для операций префиксного/постфиксного инкремента/декремента и операции типа X BINOP = EXPR , где BINOP представляет собой не перегруженный оператор +, *, -, /, &, ^, |, <<, >> . Пример использования такой директивы:
Разделение задач между потоками
Параллельный цикл
Параллельный цикл позволяет задать опцию schedule , изменяющую алгоритм распределения итераций между потоками. Всего поддерживается 3 таких алгоритма. Далее полагаем, что у нас p потоков выполняют n итераций:
В статье про параллельный цикл также описаны опции nowait и reduction . Первая из них очень редко даст ощутимый выигрыш, а вторую я рекомендую использовать как можно чаще (вместо критических секций). В той статье reduction использовалась во всех примерах и за счет этого удалось избежать явного использования критических секций, однако это удается не всегда и поэтому стоит знать что у нее внутри. Итак, параллельно вычислить сумму элементов массива можно так:
Такой подход используется постоянно, поэтому я рекомендую внимательно рассмотреть этот код. Чуть более сложным примером является параллельный поиск максимума/минимума [9]. В качестве задачи для проверки усвоения материала предлагаю попробовать построить гистограмму (например для изображения).
Параллельные задачи (parallel tasks)
Иногда такую ситуацию нелегко обнаружить, например, на нашем форуме можно найти параллельную реализацию решения СЛАУ методом Крамера, при этом параллельно вычисляется n определителей. Функция вычисления определителя вызывает функцию сведения матрицы к треугольному виду, которая может быть распараллелена.
Параллельные секции
Механизм параллельных секций видится мне достаточно низкоуровневым. Тем не менее, он полезен в ряде случаев. Как было отмечено выше, параллельный цикл можно применять только в случаях, если итерации цикла не зависят друг от друга, т.е. тут нельзя:
Заключение и дополнительная литература
Читайте также: