Учебное пособие саранск издательство свмо 2013 2 удк 004. 42 Ббк з97 Авторский знак о753
Скачать 6.58 Mb.
|
OMP_NESTED функцией omp_set_nested(), где в качестве параметра задается 0 или 1. Данная функция разрешает или запрещает вложенный параллелизм. Если вложенный параллелизм разрешён, то каждая нить, в которой встретится описание параллельной области, породит для её выполнения новую группу нитей. Сама породившая нить станет в новой группе нитью- мастером. Если система не поддерживает вложенный параллелизм, данная функция не будет иметь эффекта. Получить значение переменной OMP_NESTED можно с помощью функции omp_get_nested(). Пример 3.6. #include #include int main() { int n; omp_set_nested(1); 54 #pragma omp parallel private(n) { n=omp_get_thread_num(); #pragma omp parallel { printf("Нить %d - %d\n", n, omp_get_thread_num()); } } return 0; } Вызов функции omp_set_nested() разрешает использование вложенных параллельных областей.Каждая нить внешней параллельной области породит новые нити, каждая из которых напечатает свой номер вместе с номером породившей нити. Функция omp_get_max_threads() возвращает максимально допустимое число нитей для использования в следующей параллельной области. Функция omp_get_num_procs()возвращает количество процессоров, доступных для использования программе пользователя на момент вызова. Нужно учитывать, что количество доступных процессоров может динамически изменяться. Функция omp_in_parallel()возвращает 1, если она была вызвана из активной параллельной области программы. Переменная OMP_MAX_ACTIVE_LEVELS задаёт максимально допустимое количество вложенных параллельных областей. Значение может быть установлено при помощи вызова функции omp_set_max_active_levels(). Если значение max превышает максимально допустимое в системе, будет установлено максимально допустимое в системе значение. При вызове из параллельной области результат выполнения зависит от реализации. Значение переменной OMP_MAX_ACTIVE_LEVELS может быть получено при помощи вызова функции omp_get_max_active_levels(). Функция omp_get_level() выдаёт для вызвавшей нити количество вложенных параллельных областей в данном месте кода. При вызове из последовательной области функция возвращает значение 0. Функция omp_get_ancestor_thread_num() возвращает для уровня вложенности параллельных областей, заданного параметром level, номер нити, породившей данную нить. Если level меньше нуля или больше 55 текущего уровня вложенности, возвращается -1. Если level=0, функция вернёт 0, а если level=omp_get_level(), вызов эквивалентен вызову функции omp_get_thread_num(). Функция omp_get_team_size() возвращает для заданного параметром level уровня вложенности параллельных областей количество нитей, порождённых одной родительской нитью. Если level меньше нуля или больше текущего уровня вложенности, возвращается -1. Если level=0, функция вернёт 1, а если level=omp_get_level(), вызов эквивалентен вызову функции omp_get_num_threads(). Функция omp_get_active_level() возвращает для вызвавшей нити количество вложенных параллельных областей, обрабатываемых более чем одной нитью, в данном месте кода. При вызове из последовательной области возвращает значение 0. Переменная среды OMP_STACKSIZE задаёт размер стека для создаваемых из программы нитей. Значение переменной может задаваться в виде size | sizeB | sizeK | sizeM | sizeG, где size – положительное целое число, а буквы B, K, M, G задают соответственно, байты, килобайты, мегабайты и гигабайты. Если ни одной из этих букв не указано, размер задаётся в килобайтах. Если задан неправильный формат или невозможно выделить запрошенный размер стека, результат будет зависеть от реализации. Переменная среды OMP_WAIT_POLICY задаёт поведение ждущих процессов. Если задано значение ACTIVE, то ждущему процессу будут выделяться циклы процессорного времени, а при значении PASSIVE ждущий процесс может быть отправлен в спящий режим, при этом процессор может быть назначен другим процессам. Переменная среды OMP_THREAD_LIMIT задаёт максимальное число нитей, допустимых в программе. Если значение переменной не является положительным целым числом или превышает максимально допустимое в системе число процессов, поведение программы будет зависеть от реализации. Значение переменной может быть получено при помощи процедуры omp_get_thread_limit() [11,13,14]. 3.3.2 Директивы single и master Директива OpenMP single используется для выделения участков программы в области параллельных структурных блоков, выполняющихся только в одном из параллельных потоков. Во всех остальных параллельных потоках выделенный директивой single участок программы не выполняется, однако параллельные процессы, выполняющиеся в 56 остальных потоках, ждут завершения выполнения выделенного участка программы, т. е. неявно реализуется процедура синхронизации. Какая именно нить будет выполнять выделенный участок программы, не специфицируется. Одна нить будет выполнять данный фрагмент, а все остальные нити будут ожидать завершения её работы (при отсутствии опции nowait). #pragma omp single [опция [[,] опция]...] Рассмотрим возможные опции данной директивы. private(список) – задает список переменных, для которых порождается локальная копия в каждой нити; начальное значение локальных копий переменных из списка не определено. firstprivate(список) – задаёт список переменных, для которых порождается локальная копия в каждой нити; локальные копии переменных инициализируются значениями этих переменных в нити-мастере. copyprivate(список) – после выполнения нити, содержащей конструкцию single, новые значения переменных списка будут доступны всем одноименным частным переменным (private и firstprivate), описанным в начале параллельной области и используемым всеми её нитями; опция не может использоваться совместно с опцией nowait; переменные списка не должны быть перечислены в опциях private и firstprivateданной директивы single; nowait – после выполнения выделенного участка происходит неявная барьерная синхронизация параллельно работающих нитей: их дальнейшее выполнение происходит только тогда, когда все они достигнут данной точки; если в подобной задержке нет необходимости, опция nowait позволяет нитям, уже дошедшим до конца участка, продолжить выполнение без синхронизации с остальными [3]. Пример 3.7. #include #include int main() { #pragma omp parallel { printf("Сообщение 1\n"); #pragma omp single nowait { printf("Одна нить\n"); 57 } printf("Сообщение 2\n"); } return 0; } Данный пример иллюстрирует применение директивы single вместе с опцией nowait. Сначала все нити напечатают текст "Сообщение 1", при этом одна нить (не обязательно нить-мастер) дополнительно напечатает текст "Одна нить". Остальные нити, не дожидаясь завершения выполнения области single, напечатают текст "Сообщение 2". Таким образом, первое появление "Сообщение 2" в выводе может встретиться как до текста "Одна нить", так и после него. Если убрать опцию nowait, то по окончании области single произойдёт барьерная синхронизация, и ни одна выдача "Сообщение 2" не может появиться до выдачи "Одна нить". Директива master выделяет участок кода, который будет выполнен только нитью-мастером. Остальные нити просто пропускают данный участок и продолжают работу с оператора, расположенного следом за ним. Неявной синхронизации данная директива не предполагает. #pragma omp master Пример 3.8. #include #include int main() { int n; #pragma omp parallel private(n) { n=1; #pragma omp master { n=omp_get_thread_num(); } printf("Значение n: %d\n", n); } return 0; } 58 Пример показывает, что все потоки параллельной области выведут на экран 1, а главный поток выведет на экран свой номер, то есть 0. 3.3.3 Классы переменных В OpenMP переменные в параллельных областях программы разделяются на два основных класса: shared(общие; все нити видят одну и ту же переменную); private(локальные, приватные; каждая нить видит свой экземпляр данной переменной). Общая переменная всегда существует лишь в одном экземпляре для всей области действия и доступна всем нитям под одним и тем же именем. Объявление локальной переменной вызывает порождение своего экземпляра данной переменной (того же типа и размера) для каждой нити. Изменение нитью значения своей локальной переменной никак не влияет на изменение значения этой же локальной переменной в других нитях. Если несколько переменных одновременно записывают значение общей переменной без выполнения синхронизации или если как минимум одна нить читает значение общей переменной и как минимум одна нить записывает значение этой переменной без выполнения синхронизации, то возникает ситуация так называемой «гонки данных» (data race), при которой результат выполнения программы непредсказуем. По умолчанию, все переменные, порождённые вне параллельной области, при входе в эту область остаются общими (shared). Исключение составляют переменные, являющиеся счетчиками итераций в цикле, по очевидным причинам. Переменные, порождённые внутри параллельной области, по умолчанию являются локальными (private). Явно назначить класс переменных по умолчанию можно с помощью опции default. Не рекомендуется постоянно полагаться на правила по умолчанию, для большей надёжности лучше всегда явно описывать классы используемых переменных [5,6]. Пример 3.9. #include #include int main() { int n=1; printf("n в последовательной области (начало): %d\n", n); #pragma omp parallel private(n) { n=omp_get_thread_num(); 59 printf("Значение n на нити: %d\n", n); } printf("n в последовательной области (конец): %d\n", n); return 0; } В данном примере демонстрируется использование опции private. В последовательной области переменно n присвоено значение 1. Далее порождается параллельная область, где переменной n каждой нити присваивается номер потока и выводится на экран. После выхода из параллельной области значение n снова оказывается равным 1. Для реализации механизма передачи данных между параллельными потоками из одного параллельного структурного блока программы в другой, минуя промежуточный последовательный структурный блок в OpenMP имеется специальная директива threadprivate. 3.4 Варианты распределения работы между нитями 3.4.1 Низкоуровневое программирование Все нити в параллельной области нумеруются последовательными целыми числами от 0 до N-1, где N — количество нитей, выполняющих данную область. Можно программировать на самом низком уровне, распределяя работу с помощью функций omp_get_thread_num() и omp_get_num_threads(), возвращающих номер нити и общее количество порождённых нитей в текущей параллельной области, соответственно. Вызов функции omp_get_thread_num()позволяет нити получить свой уникальный номер в текущей параллельной области. Вызов функции omp_get_num_threads()позволяет нити получить количество нитей в текущей параллельной области. Использование функций omp_get_thread_num() и omp_get_num_threads()позволяет назначать каждой нити свой кусок кода для выполнения. Однако использование этого стиля программирования в OpenMP далеко не всегда оправдано – разработчик в этом случае должен явно организовывать синхронизацию доступа к общим данным [11]. Пример 3.10. #include #include int main() { 60 int count, num; #pragma omp parallel { count=omp_get_num_threads(); num=omp_get_thread_num(); if (num == 0) printf("Всего нитей: %d\n", count); else printf("Нить номер %d\n", num); } return 0; } В данном примере в параллельной области определен условный оператор, в результате выполнения которого главный поток выведет на экран количество нитей, а остальные потоки свой номер. 3.4.2 Распараллеливание оператора цикла Если в параллельной области встретился оператор цикла без дополнительных указаний, то он будет выполнен всеми нитями текущей группы, то есть каждая нить выполнит все итерации данного цикла. Для распределения итераций цикла между различными нитями можно использовать директиву for. Эта директива относится к следующему за ней блоку, содержащему оператор for. На языке Сидиректива выглядит следующим образом: #pragma omp for [опция [[, ] опция …] Рассмотрим опции данной директивы. private(список) – задает список переменных, для которых порождается локальная копия в каждой нити; начальное значение локальных копий переменных из списка не определено. firstprivate(список) – задаёт список переменных, для которых порождается локальная копия в каждой нити; локальные копии переменных инициализируются значениями этих переменных в нити- мастере. lastprivate(список)– переменным, перечисленным в списке, присваивается результат с последнего витка цикла. reduction(оператор:список) – определяется оператор - операции ( +, -, *, / и т. п.) или функции, для которых будут вычисляться соответствующие частичные значения в параллельных потоках последующего параллельного структурного блока; кроме того, определяется список локальных переменных, в котором будут сохраняться 61 соответствующие частичные значения; после завершения всех параллельных процессов частичные значения складываются (вычитаются, перемножаются и т. п.), и результат сохраняется в одноименной общей переменной. schedule(type[, chunk])– опция задаёт, каким образом итерации цикла распределяются между нитями. collapse(n) — опция указывает, что n последовательных тесновложенных циклов ассоциируется с данной директивой; для циклов образуется общее пространство итераций, которое делится между нитями; если опция collapseне задана, то директива относится только к одному непосредственно следующему за ней циклу. ordered – опция, говорящая о том, что в цикле могут встречаться директивы ordered; в этом случае определяется блок внутри тела цикла, который должен выполняться в том порядке, в котором итерации идут в последовательном цикле. nowait – в конце параллельного цикла происходит неявная барьерная синхронизация параллельно работающих нитей: их дальнейшее выполнение происходит только тогда, когда все они достигнут данной точки; если в подобной задержке нет необходимости, опция nowait позволяет нитям, уже дошедшим до конца цикла, продолжить выполнение без синхронизации с остальными. Предполагается, что корректная программа не должна зависеть от того, какая именно нить какую итерацию параллельного цикла выполнит. Нельзя использовать побочный выход из параллельного цикла. Если директива параллельного выполнения стоит перед набором вложенных циклов, завершающихся одним оператором, то директива действует только на самый внешний цикл [3]. Пример 3.11. #include #include int main() { int s, i, n; s=0; #pragma omp parallel private (i,n) reduction(+:s) { n=omp_get_thread_num(); #pragma omp for for (i=1;i<10;i++) { 62 s=s+i; printf("Нить %d сложила элементы с номером %d\n", n, i); } } return 0; } Пример демонстрирует нахождение суммы чисел от 1 до 10 с помощью распараллеливания цикла. На экран выводится номер нити и номера итераций цикла, которые эта нить выполнила. Параметрами опции schedule являются следующие: static – распределение итераций цикла; размер блока – chunk. Первый блок из chunk итераций выполняет нулевая нить, второй блок – следующая и т.д. до последней нити, затем распределение снова начинается с нулевой нити. Если значение chunk не указано, то всё множество итераций делится на непрерывные куски примерно одинакового размера (конкретный способ зависит от реализации), и полученные порции итераций распределяются между нитями. dynamic – динамическое распределение итераций с фиксированным размером блока: сначала каждая нить получает chunk итераций (по умолчанию chunk=1), та нить, которая заканчивает выполнение своей порции итераций, получает первую свободную порцию из |