1. Знайомство з Open MP 1.1 Теоретичні відомості
Компіляція Для використання механізмів OpenMP потрібно скомпілювати програму компілятором , що підтримує OpenMP , із зазначенням відповідного ключа Компілятор / платформа
Компілятор
Параметр
Intel Linux Opteron/Xeon
icc icpc ifort
-openmp
PGI Linux Opteron/Xeon
pgcc pgCC pgf77 pgf90
-mp
GNU Linux Opteron/Xeon IBM Blue Gene
gcc g++ g77 gfortran
-fopenmp
IBM Blue Gene
bgxlc_r, bgcc_r bgxlC_r, -qsmp=omp bgxlc++_r, bgxlc89_r,bgxlc99_r, bgxlf_r, bgxlf 90_r, bgxlf95_r, bgxlf2003_r * Будьте певні що використовуєте потоко-безпечні компілятори - його ім'я закінчується на_r
Компілятор з підтримкою OpenMP визначає макрос _OPENMP , який може використовуватися для умовної компіляції окремих блоків, характерних для паралельної версії програми. Для перевірки того, що компілятор підтримує будь-яку версію OpenMP, достатньо написати директиви умовної компіляції # ifdef або # ifndef. Найпростіші приклади умовної компіляції в програмах наведені в прикладі 1. Приклад 1 # include int main () { # ifdef _OPENMP printf (" OpenMP is supported ! \ n ") ; # endif } Розпаралелювання можна виконати просто взявши одно-потокову програму і додавши директиви компілятора або повної вставки підпрограм для встановлення кількох рівнів паралелізму, замикань і навіть вкладених замикань. Модель розгалужень-об’єднань вказана на рисунку 1.
Усі програми OpenMP починаються з одного процесу: основного потоку. Головний потік виконується послідовно допоки не зустрічає першу конструкцію паралельної частини програми. РОЗГАЛУЖЕННЯ: основний потік створює групу паралельних потоків. На багатьох потоках виконуються оператори програми, що знаходяться в паралельній частині програми. ОБ'ЄДНАННЯ: Коли група потоків закінчує виконання операторів з паралельної частини, то потоки синхронізуються і зупиняються, залишаючи єдиний основний протік. Кількість паралельних частин і потоків, що їх виконують у програмі є довільною. Значна частина функціональності OpenMP реалізується за допомогою директив компілятору. Вони повинні бути явно вставлені користувачем, що дозволить виконувати програму в паралельному режимі. Директиви OpenMP в програмах на мові Сі - вказівками препроцесору, що починаються з #pragma omp. Ключове слово omp використовується для того, щоб виключити випадкові збіги імен директив OpenMP з іншими іменами. Основні правила: Враховується регістр Директиви використовуються згідно дійсних стандартів C/C++ щодо використання директив Лише одне ім'я директиви може бути використано як директива Кожна директива застосовується до наступного оператора, який може бути структурованим блоком операцій. Стрічка з довгим записом директиви може бути продовжена в наступних стрічках, якщо вона закінчується символом "\". В таблиці вказано формат директив в мові С
#pragma omp
Ім'я директиви
[пункт...]
нова стрічка
Необхідно використовувати для всіх директив OpenMP в C/C++.
Будь-яка правильна директива OpenMP. Повинна знаходитись після pragma але перед пунктом.
Додатковий параметр. Пункти можуть знаходитись і повторюватись в разі необхідності (якщо повторення не заборонено).
Необхідна частина. Структурний блок, що виконується цією директивою.
Паралельні і послідовні області У момент запуску програми породжується єдина нитка - майстер або « основна » нитка , яка починає виконання програми з першого оператора. Основна нитку і тільки вона виконує всі послідовні області програми . При вході в паралельну область породжуються додаткові нитки. Паралельна область задається за допомогою директиви.
Сі :
# pragma omp parallel * опція * *,+ опція + ...+ Можливі опції :
If (умова ) - виконання паралельної області за умовою. входження в паралельну область здійснюється тільки при виконанні деякої умови . Якщо умова не виконана , то директива не спрацьовує і триває обробка програми в колишньому режимі ; Num_threads ( цілочисельне вираз) - явне завдання кількості ниток , які виконуватимуть паралельну область; за замовчуванням вибирається останнє значення , встановлене за допомогою функції omp_set_num_threads ( ) , або значення змінної OMP_NUM_THREADS ; Default ( private | firstprivate | shared | none ) - Пункт DEFAULT дозволяє користувачу визначити видимість змінних за замовчуванням в межах кожного паралельного регіону. всім змінним в паралельній області, яким явно не призначений клас , буде призначений клас private , firstprivate або shared відповідно; None – всім змінним у паралельній області клас повинен бути призначений явно ; Private (список ) - задає список змінних , для яких створюється локальна копія в кожної нитки ; початкове значення локальних копій змінних зі списку не визначено ; Firstprivate (список ) - задає список змінних , для яких створюється локальна копія в кожної нитки ; локальні копії змінних ініціалізуються значеннями цих змінних в нитки - майстра ; Shared (список ) - задає список змінних , загальних для всіх ниток ; Copyin (список ) - задає список змінних , оголошених як threadprivate , які при вході в паралельну область инициализируются значеннями відповідних змінних в нитки - майстра; Reduction (оператор : список ) - задає оператор і список загальних змінних; для кожної змінної створюються локальні копії в кожної нитки ; локальні копії ініціалізуються відповідно до типу оператора ( для адитивних операцій - 0 або його аналоги , для мультиплікативних операцій - 1 або її аналоги) ; над локальними копіями змінних після виконання всіх операторів паралельної області виконується заданий оператор ; оператор (для мови Сі - + , * , - , & , | , ^ , && , | |). Порядок виконання операторів не визначений , тому результат може відрізнятися від запуску до запуску.
При вході в паралельну область породжуються нові OMP_NUM_THREADS – 1 ниток , кожна нитка отримує свій унікальний номер , причому породжує нитка отримує номер 0 і стає основною ниткою групи ( « майстром »). Решта ниток отримують в якості номера цілі числа з 1 до OMP_NUM_THREADS - 1 . Кількість ниток , що виконують дану паралельну область , залишається незмінним до моменту виходу з області. При виході з паралельної області виробляється неявна синхронізація і знищуються всі нитки , крім тої, яка їх породила. Всі породжені нитки виконують один і той же код , відповідний паралельної області. Передбачається , що в SMP-системі нитки будуть розподілені по різних процесорам (однак це , як правило , перебуває у віданні операційної системи). Приклад 2 демонструє використання директиви parallel . В результаті виконання нитка - майстер надрукує текст " Послідовна область 1 " , потім за директивою parallel породжуються нові нитки , кожна з яких надрукує текст " Паралельна область" , потім породжені нитки завершуються і залишилася нитка - майстер надрукує текст " Послідовна область 2". Приклад 2 # include int main ( int argc , char * argv [])
{ printf ( " Послідовна область 1 \ n " ) ; # pragma omp parallel { printf ( " Паралельна область \ n " ) ; } printf ( " Послідовна область 2 \ n " ) ; }
reduction Приклад 3 демонструє застосування опції reduction . У даному прикладі проводиться підрахунок загальної кількості породжених ниток. кожна нитка ініціалізує локальну копію змінної count значенням 0 . далі , кожна нитка збільшує значення власної копії змінної count на одиницю і виводить отримане число. На виході з паралельної області відбувається підсумовування значень змінних count по всіх ниткам , і отримана величина стає новим значенням змінної count в послідовній області. Приклад 3 # include int main ( int argc , char * argv []) { int count = 0 ; # pragma omp parallel reduction ( + : count ) { count + + ; printf ( " Поточне значення count : % d \ n " , count ) ; } printf ( " Число ниток : % d \ n " , count ) ; } Обмеження: Змінні в списку повинні бути іменованими скалярними змінними. Вони не можуть бути масивом чи змінною структурного типу. Також вони повинні бути оголошені в SHARED блоці. Операції скорочення не є асоціативними для нецілочисельних значень. Пункт REDUCTION призначений для використання в областях або конструкцій з розподілом роботи, де скорочені змінні використовуються в виразах, що мають одну з наступних форм:
C / C++ x = x op expr
x = expr op x (except subtraction) x binop = expr x++ ++x x---x x є скалярною змінною в списку expr є скалярним виразом що не посилається на x op не перевантажений і є один з операторів +, *, -, /, &, ^, |, &&, || binop не перевантажений і є один з операторів +, *, -, /, &, ^, |
Замір часу У OpenMP передбачені функції для роботи з системним таймером. Функція omp_get_wtime () повертає в викликала нитки астрономічне час у секундах ( дійсне число подвійної точності ), що пройшли з деякого моменту в минулому:
double omp_get_wtime (void); Якщо деякий ділянку програми оточити викликами даної функції , то різниця повертаються значень покаже час роботи даної ділянки. Гарантується , що момент часу, який використовується в якості точки відліку , не буде змінений за час існування процесу . Таймери різних ниток можуть бути не синхронізовані і видавати різні значення . Функція omp_get_wtick () повертає в викликала нитки дозвіл таймера в секундах. Цей час можна розглядати як міру точності таймера :
double omp_get_wtick (void); Приклад 4 ілюструє застосування функцій omp_get_wtime ( ) і omp_get_wtick ( ) для роботи з таймерами в OpenMP . У даному прикладі проводиться вимір початкового часу , потім відразу завмер кінцевого часу . Різниця часів дає час на замір часу. Крім того , вимірюється точність системного таймера. Приклад 4 # include # include int main ( int argc , char * argv []) { double start_time , end_time , tick ; start_time = omp_get_wtime (); end_time = omp_get_wtime (); tick = omp_get_wtick (); printf ( " Час на замір часу % lf \ n " , end_time - start_time ) ; printf ( "Точність таймера % lf \ n " , tick ) ; }
Кількість процесів Кількість потоків, в паралельній області визначається за такими факторами, в порядку пріоритетності: 1. 2. 3. 4. 5.
Аналіз пункту IF Встановлення опції NUM_THREADS Використання бібліотечної функції omp_set_num_threads() Встановлення змінної середовища OMP_NUM_THREADS Реалізація за замовчуванням - зазвичай кількість процесорів на вузлі, хоча цей параметр може бути динамічним (див. наступний підпункт).
Значення змінної середовища OMP_NUM_THREADS можна задати перед запуском програми. Наприклад, на сервері в командній оболонці це можна зробити за допомогою наступної команди: export OMP_NUM_THREADS=n З програми її можна змінити за допомогою виклику функції omp_set_num_threads( ) .
void omp_set_num_threads ( int num ); Приклад 5 демонструє застосування функції omp_set_num_threads ( ) і опції num_threads. Перед першою паралельної областю викликом функції omp_set_num_threads ( 2 ) виставляється кількість ниток , рівне 2 . але до першої паралельної області застосовується опція num_threads ( 3 ) , яка вказує , що дану область слід виконувати трьома нитками. отже , повідомлення " Паралельна область 1 " буде виведено трьома нитками. До другої паралельної області опція num_threads не застосовується , до неї діє значення, встановлене функцією omp_set_num_threads ( 2 ) , і повідомлення " Паралельна область 2 " буде виведено двома нитками. Приклад 5 # include # include int main ( int argc , char * argv []) { omp_set_num_threads ( 2 ) ; # pragma omp parallel num_threads ( 3 ) { printf ( " Паралельна область 1 \ n " ) ; } # pragma omp parallel { printf ( " Паралельна область 2 \ n " ) ; } } У деяких випадках система може динамічно змінювати кількість ниток , що використовуються для виконання паралельної області , наприклад , для оптимізації використання ресурсів системи . Це
дозволено робити , якщо змінна середовища OMP_DYNAMIC встановлена в true . Наприклад , в на сервері в командній оболонці її можна встановити за допомогою наступної команди: export OMP_DYNAMIC=true У системах з динамічною зміною кількості ниток значення за замовчуванням не визначено , інакше значення за замовчуванням: false . Змінну OMP_DYNAMIC можна встановити за допомогою функції omp_set_dynamic ( ) . void omp_set_dynamic ( int num ) ; На мові Сі в якості значення параметра функції omp_set_dynamic ( ) задається 0 або 1 , а мовою. Якщо система не підтримує динамічна зміну кількості ниток , то при виклику функції omp_set_dynamic ( ) значення змінної OMP_DYNAMIC не зміниться.
Дізнатися значення змінної OMP_DYNAMIC можна за допомогою функції omp_get_dynamic ( ) . Сі : int omp_get_dynamic ( void ) ; Приклад 6 демонструє застосування функцій omp_set_dynamic ( ) і omp_get_dynamic ( ) . Спочатку роздруковується значення, отримане функєю omp_get_dynamic ( ) - це дозволяє дізнатися значення змінної OMP_DYNAMIC за замовчуванням. Потім за допомогою функції omp_set_dynamic ( ) змінна OMP_DYNAMIC встановлюється в true , що підтверджує видача ще один раз значення функції omp_get_dynamic ( ) . Потім породжується паралельна область , яка виконується заданою кількістю ниток ( 128). У паралельній області друкується реальне число процесів. Директива master дозволяє забезпечити друк тільки процесом - майстром. У системах з динамічною зміною числа ниток видане значення може відрізнятися від заданого ( 128). Приклад 6 # include # include int main ( int argc , char * argv []) { printf ( " Значення OMP_DYNAMIC : % d \ n " , omp_get_dynamic ( )) ; omp_set_dynamic ( 1 ) ; printf ( " Значення OMP_DYNAMIC : % d \ n " , omp_get_dynamic ( )) ; # pragma omp parallel num_threads ( 128 ) { # pragma omp master { printf ( " Паралельна область , % d ниток \ n " , omp_get_num_threads ( )) ; } }
} Функція omp_get_max_threads () повертає максимально допустиме число ниток для використання в наступній паралельної області. Сі : int omp_get_max_threads ( void ) ; Функція omp_get_num_procs () повертає кількість процесорів , доступних для використання програмі користувача на момент виклику. Але потрібно враховувати , що кількість доступних процесорів може динамічно змінюватися. Сі : int omp_get_num_procs ( void ) ;
Вкладені паралельні області Паралельні області можуть бути вкладеними. За замовчуванням вкладена паралельна область виконується однією ниткою. Це управляється встановленням змінної середовища OMP_NESTED . Наприклад за допомогою наступної команди : export OMP_NESTED = true Змінити значення змінної OMP_NESTED можна за допомогою виклику функції omp_set_nested ( ) . Сі :
void omp_set_nested ( int nested ) Функція omp_set_nested ( ) дозволяє або забороняє вкладений паралелізм . На мові Сі в якості значення параметра задається 0 або 1 . Якщо вкладений паралелізм дозволений, то кожна нитка , в якій зустрінеться опис паралельної області , породить для її виконання нову групу ниток. Сама породила нитка стане в новій групі ниткою - майстром. Якщо система не підтримує вкладений паралелізм , дана функція не матиме ефекту. Приклад 7 демонструє використання вкладених паралельних областей і функції omp_set_nested (). Виклик функції omp_set_nested ( ) перед першою частиною дозволяє використання вкладених паралельних областей. Для визначення номера нитки в поточній паралельної секції використовуються виклики функції omp_get_thread_num ( ) . Кожна нитка зовнішньої паралельної області породить нові нитки, кожна з яких надрукує свій номер разом з номером породила нитки. Далі виклик omp_set_nested ( ) забороняє використання вкладених паралельних областей. У другій частині вкладена паралельна область буде виконуватися без породження нових ниток , що й видно по одержуваної видачі. Приклад 7 # include # include int main ( int argc , char * argv []) { int n ;
omp_set_nested ( 1 ) ; # pragma omp parallel private ( n ) { n = omp_get_thread_num (); # pragma omp parallel { printf ( "Частина 1 , нитка % d - % d \ n " , n , omp_get_thread_num ( )) ; } } omp_set_nested (0); # pragma omp parallel private ( n ) { n = omp_get_thread_num (); # pragma omp parallel { printf ( "Частина 2 , нитка % d - % d \ n " , n , omp_get_thread_num ( )) ; } } }
Дізнатися значення змінної OMP_NESTED можна за допомогою функції omp .
_get_nested ( )
Сі : int omp_get_nested ( void ) ;
Звідки була викликана функція? omp_in_parallel () Функція omp_in_parallel () повертає 1 , якщо вона була викликана з активної паралельної області програми . Сі : int omp_in_parallel ( void ) ;
Приклад 8 ілюструє застосування функції omp_in_parallel ( ) . Моде демонструє зміну функціональності в залежності від того , викликана вона з послідовної або з паралельної області. У послідовній області буде надруковано " Послідовна область" , а в паралельній - " Паралельна область" . Приклад 8 # include # include void mode ( void ) { if ( omp_in_parallel ( )) printf ( " Паралельна область \ n " ) ; else printf ( " Послідовна область \ n " ) ; } int main ( int argc , char * argv []) { mode (); # pragma omp parallel { # pragma omp master { mode (); } } }
Директива single Якщо в паралельній області небудь ділянку коду повинен бути виконуватись лише один раз , то його потрібно виділити директивами single. Вона також може знадобитись при роботі з "потоконебезпечним" кодом (наприклад операції ВВОДУ /ВИВОДУ) Сі : #pragma omp single [clause ...] newline private (list) firstprivate (list) nowait copyprivate structured_block
nowait Можливі опції :
private (список ) - задає список змінних , для яких породжується локальна копія в кожної нитки ; початкове значення локальних копій змінних зі списку не визначено ; firstprivate (список ) - задає список змінних , для яких породжується локальна копія в кожної нитки ; локальні копії змінних ініціалізуються значеннями цих змінних в нитки майстра ; copyprivate (список) - після виконання нитки , що містить конструкцію single , нові значення змінних списку будуть доступні всім однойменною приватним змінним ( private і firstprivate ) , які описані на початку паралельної області і використовуваним усіма її нитками; опція не може використовуватися спільно з опцією nowait ; змінні списку не повинні бути перераховані в опціях private і firstprivate даної директиви single ; nowait - після виконання виділеної ділянки відбувається неявна бар'єрна синхронізація паралельно працюючих ниток : їх подальше виконання відбувається тільки тоді , коли всі вони досягнуть даної точки ; якщо в подібній затримці немає необхідності , опція nowait дозволяє ниткам , вже дійшли до кінця ділянки , продовжити виконання без синхронізації з іншими.
Яка саме нитка буде виконувати виділену ділянка програми не задано . Одна нитка буде виконувати цей фрагмент , а всі інші нитки чекатимуть завершення її роботи , якщо тільки не вказана опція nowait . Необхідність використання директиви single часто виникає при роботі із загальними змінними. Приклад 9 ілюструє застосування директиви single разом з опцією nowait . Спочатку всі нитки надрукують текст " Повідомлення 1 " , при цьому одна нитка ( не обов'язково нитка - майстер ) додатково надрукує текст " Одна нитка " . Решта нитки , не чекаючи завершення виконання області single , надрукують текст " Повідомлення 2". Таким чином , перша поява " Повідомлення 2 " у виведенні може зустрітися як до тексту " Одна нитка" , так і після нього. Якщо прибрати опцію nowait , то по закінченні області single відбудеться бар'єрна синхронізація , і жодна видача " Повідомлення 2" не може з'явитися до видачі " Одна нитка" . Приклад 9 # include int main ( int argc , char * argv []) { # pragma omp parallel { printf ( " Повідомлення 1 \ n " ) ; # pragma omp single nowait { printf ( " Одна нитка \ n " ) ; }
printf ( " Повідомлення 2 \ n " ) ; } }
copyprivate Приклад 10 ілюструє застосування опції copyprivate . У ньому змінна n оголошена в паралельній області як локальна . кожна нитка присвоїть змінній n значення, рівне своєму порядковому номеру , і надрукує дане значення . В області single одна з ниток присвоїть змінній n значення 100 , і на виході з області це значення буде присвоєно змінної n на всіх нитках. Наприкінці паралельної області значення n друкується ще раз і на всіх нитках воно дорівнює 100 . Приклад 10 # include # include int main ( int argc , char * argv []) { int n ; # pragma omp parallel private ( n ) { n = omp_get_thread_num (); printf ( " Значення n (початок ) : % d \ n " , n ) ; # pragma omp single copyprivate ( n ) { n = 100 ; } printf ( " Значення n (кінець ) : % d \ n " , n ) ; } }
Директива master Директиви master виділяють ділянку коду , которий буде виконаний тільки ниткою - майстром. Решта нитки просто пропускають дану ділянку і продовжують роботу з оператора , розташованого слідом за ним. Директива передбачає неявну синхронізацію. Сі : # pragma omp master Приклад 11 демонструє застосування директиви master . Змінна n є локальною, тобто кожна нитка працює зі своїм екземпляром. Спочатку всі нитки присвоять змінній n значення 1. Потім нитка майстер присвоїть змінній n значення 2 , і всі нитки надрукують значення n . Потім нитка - майстер
присвоїть змінній n значення 3 , і знову всі нитки надрукують значення n . Видно , що директиву master завжди виконує одна і та ж нитку. У даному прикладі всі нитки виведуть значення 1 , а нитка - майстер спочатку виведе значення 2 , а потім - значення 3 . Приклад 11 # include int main ( int argc , char * argv []) { int n ; # pragma omp parallel private ( n ) { n=1; # pragma omp master { n=2; } printf ( " Перше значення n : % d \ n " , n ) ; # pragma omp barrier # pragma omp master { n=3; } printf ( " Друге значення n : % d \ n " , n ) ; } }
Модель даних Модель даних в OpenMP припускає наявність як загальної для всіх ниток області пам'яті , так і локальній області пам'яті для кожної нитки . У OpenMP змінні в паралельних областях програми поділяються на два основні класи:
Shared (загальні ; всі нитки бачать одну й ту ж змінну ) ; Private (локальні , приватні ; кожна нитка бачить свій екземпляр даної змінної ) . Загальна змінна завжди існує лише в одному екземплярі для всієї області дії і доступна всім ниткам під одним і тим же ім'ям. Об'явлення локальної змінної викликає породження свого примірника даної змінної ( того ж типу і розміру) для кожної нитки . зміна ниткою значення своєї локальної змінної ніяк не впливає на зміну значення цієї ж локальної змінної в інших нитках.
Якщо кілька змінних одночасно записують значення загальної змінної без виконання синхронізації або якщо як мінімум одна нитка читає значення загальної змінної і як мінімум одна нитка записує значення цієї змінної без виконання синхронізації , то виникає ситуація так званої « гонки даних» ( data race ) , при якій результат виконання програми непередбачуваний. За замовчуванням, всі змінні , породжені поза паралельної області , при вході в цю область залишаються загальними ( shared ) . виняток становлять змінні , які є лічильниками ітерацій в циклі , з очевидних причин. Змінні , породжені всередині паралельної області , за замовчуванням є локальними ( private ) . Явно призначити клас змінних за замовчуванням можна за допомогою опції default . Не рекомендується постійно покладатися на правила за замовчуванням , для більшої надійності краще завжди явно описувати класи використовуваних змінних , вказуючи в директивах OpenMP опції private , shared , firstprivate , lastprivate , reduction . Приклад 12 демонструє використання опції private . У ньому змінна n оголошена як локальна змінна в паралельній області. Це означає , що кожна нитка буде працювати зі своєю копією змінної n , при цьому на початку паралельної області на кожної нитки змінна n НЕ БУДЕ ініціалізована . У ході виконання програми значення змінної n буде виведено в чотирьох різних місцях. Перший раз значення n буде виведено в послідовній області , відразу після присвоювання змінної n значення 1 . Другий раз всі нитки виведуть значення своєї копії змінної n на початку паралельної області , неініціалізованих значення може залежати від реалізації . Далі всі нитки виведуть свій порядковий номер , отриманий за допомогою функції omp_get_thread_num ( ) і присвоєний змінної n . Після завершення паралельної області буде ще раз виведено значення змінної n , яке виявиться рівним 1 ( не змінилося під час виконання паралельної області) . Приклад 12 # include # include int main ( int argc , char * argv []) { int n = 1 ; printf ( " n в послідовній області (початок ) : % d \ n " , n ) ; # pragma omp parallel private ( n ) { printf ( " Значення n на нитки ( на вході) : % d \ n " , n ) ; / * Привласнимо змінної n номер поточної нитки * / n = omp_get_thread_num (); printf ( " Значення n на нитки ( на виході) : % d \ n " , n ) ; } printf ( " n в послідовній області (кінець ) : % d \ n " , n ) ; }
Приклад 13 демонструє використання опції shared . Масив m оголошений загальним для всіх ниток. На початку послідовної області масив m заповнюється нулями і виводиться на друк. У паралельній області кожна нитка знаходить елемент , номер якого збігається з порядковим номером нитки в загальному масиві , і привласнює цьому елементу значення 1. Далі , в послідовній області друкується змінений масив m . Приклад 12 # include # include int main ( int argc , char * argv []) { int i , m [ 10 ] ; printf ( " Масив m на початку : \ n " ) ; / * Заполним масив m нулями і надрукуємо його * / for ( i = 0 ; i < 10 ; i + +) { m[i]=0; printf ( " % d \ n " , m [ i ] ) ; } # pragma omp parallel shared ( m ) { / * Привласнимо 1 елементу масиву m , номер якого збігається з номером поточний нитки * / m [ omp_get_thread_num ( )] = 1 ; } / * Ще раз надрукуємо масив * / printf ( " Масив m наприкінці : \ n " ) ; for ( i = 0 ; i < 10 ; i + +) printf ( " % d \ n " , m [ i ] ) ; } Окремі правила визначають призначення класів змінних при вході і виході з паралельної області або паралельного циклу при використанні опцій reduction , firstprivate , lastprivate , copyin .
FIRSTPRIVATE Пункт FIRSTPRIVATE комбінує поведінку пункту PRIVATE з автоматичною ініціалізацією змінних в списку. Змінні зі списку ініціалізуються відповідно до значення їхніх основних об'єктів перед входом в конструкцію з розпаралелюванням чи розподілом роботи. Приклад 14 демонструє використання опції firstprivate . Змінна n оголошена як firstprivate в паралельній області. Значення n буде виведено в чотирьох різних місцях. Перший раз значення n буде виведено в послідовної області відразу після ініціалізації . Другий раз всі нитки виведуть значення своєї копії змінної n на початку паралельної області , і це значення буде дорівнює 1 .
Далі, за допомогою функції omp_get_thread_num ( ) всі нитки присвоять змінної n свій порядковий номер і ще раз виведуть значення n . У послідовній області буде ще раз виведено значення n , яке знову виявиться рівним 1 . Приклад 14 # include # include int main ( int argc , char * argv []) { int n = 1 ; printf ( " Значення n на початку : % d \ n " , n ) ; # pragma omp parallel firstprivate ( n ) { printf ( " Значення n на нитки ( на вході) : % d \ n " , n ) ; / * Привласнимо змінної n номер поточної нитки * / n = omp_get_thread_num (); printf ( " Значення n на нитки ( на виході) : % d \ n " , n ) ; } printf ( " Значення n наприкінці : % d \ n " , n ) ; }
threadprivate Директива threadprivate вказує , що змінні зі списку повинні бути розмножені так, щоб кожна нитка мала свою локальну копію, тобто вона робить глбальні файлові змінні локальними і стійкими для потоку, який використовується на багатократних паралельних областях. Сі : # pragma omp threadprivate (список) Для коректного використання локальних копій глобальних об'єктів потрібно гарантувати , що вони використовуються в різних частинах програми одними і тими ж нитками. Якщо на локальні копії посилаються в різних паралельних областях , то для збереження їх значень необхідно , щоб не було охоплюючих паралельних областей , кількість ниток в обох областях збігалося , а змінна OMP_DYNAMIC була встановлена в false з початку першої області до початку другої . Змінні , оголошені як threadprivate , не можуть використовуватися в опціях директив OpenMP , крім copyin, copyprivate , schedule , num_threads , if . Нижче наведена таблиця порівняння threadprivate і private.
Елемент даних
PRIVATE
THREADPRIVATE
C/C++: Змінна Fortran: змінна або загальний блок
C/C++: Змінна Fortran: загальний блок
Де оголошена
На початку області з розподілом роботи
У оголошенні кожної підпрограми використовуючи блок або глобальну файлову видимість
Стійка?
Ні
Так
Протяжність
Тільки лексична - доки не прийнятий аргумент підпрограмою
Динамічна
Ініціалізація
Використовуючи FIRSTPRIVATE
Використовуючи COPYIN
Приклад 15 демонструє використання директиви threadprivate . Глобальна змінна n оголошена як threadprivate змінна. Значення змінної n виводиться в чотирьох різних місцях. Перший раз всі нитки виведуть значення своєї копії змінної n на початку паралельної області, і це значення дорівнюватиме 1 на головній нитці та 0 на інших нитках. далі з допомогою функції omp_get_thread_num ( ) всі нитки присвоять змінній n свій порядковий номер і виведуть це значення. Потім в послідовній області буде ще раз виведено значення змінної n , яке виявиться рівним порядковому номеру головної нитки, тобто 0 . Востаннє значення змінної n виводиться в новій паралельної області , причому значення кожної локальної копії має зберегтися . Приклад 15 # include # include int n ; # pragma omp threadprivate ( n ) int main ( int argc , char * argv []) { int num ; n=1; # pragma omp parallel private ( num ) { num = omp_get_thread_num (); printf ( " Значення n на нитки % d ( на вході) : % d \ n " , num , n ) ; / * Привласнимо змінної n номер поточної нитки * / n = omp_get_thread_num (); printf ( " Значення n на нитки % d (на виході) : % d \ n " , num , n ) ; } printf ( " Значення n ( середина) : % d \ n " , n ) ;
# pragma omp parallel private ( num ) { num = omp_get_thread_num (); printf ( " Значення n на нитки % d (ще раз): % d \ n " , num , n ) ; } } Замітки:
При першому вході в паралельну область дані в THREADPRIVATE-змінних будуть вважатись невизначеними, якщо пункт COPYIN не зазначений в директиві PARALLEL THREADPRIVATE-змінні відрізняються від змінних PRIVATE (які будуть описані нижче) тому що вони можуть існувати між різними паралельними секціями коду.
Обмеження: Дані в THREADPRIVATE об'єкти гарантовано будуть збережені, тільки якщо механізм динамічних потоків "вимкнуто" і кількість потоків в різних паралельних областях залишається незмінною. За замовчуванням динамічні потоки є не визначеними. Директива THREADPRIVATE повинна бути після кожного оголошення приватної потокової змінної чи блоку.
Якщо необхідно змінну , оголошену як threadprivate , ініціалізувати значенням змінної , яка розмножується з нитки -майстра, то на вході в паралельну область можна використовувати опцію copyin . Якщо значення локальної змінної або змінною , оголошеної як threadprivate , необхідно переслати від однієї нитки всім , працюють в даній паралельної області , для цього можна використовувати опцію copyprivate директиви single . Приклад 16 демонструє використання опції copyin . Глобальна змінна n визначена як threadprivate . Застосування опції copyin дозволяє ініціалізувати локальні копії змінної n початковим значенням нитки - майстра. Всі нитки виведуть значення n , рівне 1 . Приклад 16 # include int n ; # pragma omp threadprivate ( n ) int main ( int argc , char * argv []) { n=1; # pragma omp parallel copyin ( n ) { printf ( " Значення n : % d \ n " , n ) ;
} } Примітка: Змінна основного потоку використовується як джерело копіювання. Група потоків ініціалізується значенням цієї змінної після входу в паралельну область.
Розподіл роботи OpenMP пропонує кілька варіантів розподілу роботи між запущеними нитками. Конструкції розподілу робіт в OpenMP не породжують нових ниток.
Низькорівневе розпаралелювання Всі нитки в паралельній області нумеруються послідовними цілими числами від 0 до N- 1 , де N кількість ниток , що виконують дану область . Можна програмувати на найнижчому рівні , розподіляючи роботу за допомогою функцій omp_get_thread_num ( ) і omp_get_num_threads ( ) , які повертають номер нитки і загальну кількість породжених ниток в поточній паралельної області , відповідно. Виклик функції omp_get_thread_num ( ) дозволяє нитки отримати свій унікальний номер в поточній паралельної області. Сі :
int omp_get_thread_num ( void ) ; Виклик функції omp_get_num_threads ( ) дозволяє нитці отримати кількість підниток в поточній паралельної області. Сі :
int omp_get_num_threads ( void ) ; Приклад 17 демонструє роботу функцій omp_get_num_threads ( ) і omp_get_thread_num ( ) . Нитка , порядковий номер якої дорівнює 0 , надрукує загальну кількість породжених ниток , а інші нитки надрукують свій порядковий номер . Приклад 17 # include # include int main ( int argc , char * argv []) { 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 ) ; }
} Використання функцій omp_get_thread_num ( ) і omp_get_num_threads ( ) дозволяє призначати кожної нитці свій шматок коду для виконання , і таким чином розподіляти роботу між нитками в стилі технології MPI . Однак використання цього стилю програмування в OpenMP далеко не завжди виправдано - програміст в цьому випадку повинен явно організовувати синхронізацію доступу до загальних даних . Інші способи розподілу робіт в OpenMP забезпечують значну частину цієї роботи автоматично.
Паралельні цикли Якщо в паралельній області зустрівся оператор циклу , то , відповідно до загального правила , він буде виконаний всіма нитками поточної групи, тобто кожна нитка виконає всі ітерації даного циклу . Для розподілу ітерацій циклу між різними нитками можна використовувати директиву for. Передбачається, що паралельна область вже ініціалізована, інакше виконання відбудеться послідовно на одному процесорі. Сі : #pragma omp for [clause ...] newline schedule (type [,chunk]) ordered private (list) firstprivate (list) lastprivate (list) shared (list) reduction (operator: list) collapse (n) nowait for_loop
Опції :
private (список ); firstprivate (список ); lastprivate (список) - змінним , перерахованим у списку , присвоюється результат з останнього витка циклу; reduction (оператор : список ); schedule ( type [ , chunk ]) - опція задає , яким чином ітерації циклу розподіляються між нитками; collapse ( n ) - опція вказує , що n послідовних тесновложенних циклів асоціюється з даною директивою ; для циклів утворюється загальний простір ітерацій , яке ділиться між нитками; якщо опція collapse не задане , то директива відноситься тільки до одного безпосередньо наступному за нею циклу ; ordered - опція , що говорить про те , що в циклі можуть зустрічатися директиви ordered ; в цьому випадку визначається блок всередині тіла циклу , який повинен виконуватися в тому порядку , в якому ітерації йдуть в послідовному циклі ; nowait;
На вигляд паралельних циклів накладаються досить жорсткі обмеження:
коректна програма не повинна залежати від того , яка саме нитка яку ітерацію паралельного циклу виконає .
Заборонено використовувати оператор goto з міткою, що знаходиться за межами циклу зв'язаного з директивою for.
Розмір блоку ітерацій , зазначений в опції schedule , не повинен змінюватися в рамках циклу. Розмір chunk повинен бути зазначений як цілочисельний незмінюваний вираз циклу, оскільки відсутня синхронізація його оцінки на різних потоках. Пункти ORDERED, COLLAPSE і SCHEDULE можуть вказуватись тільки по одному разу кожен.
Якщо директива паралельного виконання стоїть перед декількома циклами , що завершуються одним оператором , то директива діє тільки на зовнішній цикл . Ітеративна змінна циклу, який розпаралелюється за змістом повинна бути локальною тому, в разі , якщо вона специфікована як загальна , то вона неявно робиться локальною при вході в цикл. Після завершення циклу значення ітеративної змінної циклу не визначено, якщо не вказано в опції lastprivate . Приклад 18 демонструє використання директиви for. У послідовній області ініціалізуються три вихідних масиви A , B , C. У паралельній області ці масиви оголошені загальними. Допоміжні змінні i і n оголошені локальними . Кожна нитка присвоїть змінній n свій порядковий номер . Далі за допомогою директиви for визначається цикл , ітерації якого будуть розподілені між існуючими нитками. На кожній i - ій ітерації цикл додасть i -ті елементи масивів A і B і результат запише в i - ий елемент масиву C. Також на кожній ітерації буде надруковано номер нитки , що виконала дану ітерацію . # include # include int main ( int argc , char * argv []) { int A [ 10 ] , B [ 10 ] , C [ 10 ] , i , n ; / * Заполним вихідні масиви * / for ( i = 0 ; i < 10 ; i + +) { A[i]=i;B[i]=2*i;C[i]=0; } # pragma omp parallel shared (A , B , C) private ( i , n ) { / * Отримаємо номер поточної нитки * / n = omp_get_thread_num (); # pragma omp for for ( i = 0 ; i < 10 ; i + +) {
C[i]=A[i]+B[i]; printf ( " Нитка % d склала елементи з номером % d \ n " , n , i ) ; } } }
Schedule(Static, Dynamic, Guided, Auto, Auto ) В опції schedule параметр type задає наступне тип розподілу ітерацій : Static - ітерації циклу діляться на частини з розміром chunk (див. лістинг) і після статично призначаються на потоки. Перший блок з chunk ітераціями виконує нульова нитка , другий блок - наступна і т.д. до останньої ниткою, потім розподіл розпоинається знову з головної нитки. Якщо значення chunk НЕ вказано , то ітерації ділиться на шматки приблизно однакового розміру ( конкретний спосіб залежить від реалізації) , і отримані порції ітерацій розподіляються між нитками . Dynamic – ітерації діляться на частини з розміром параметра chunk і динамічно розподіляються між потоками.: спочатку кожна нитка отримує chunk ітерацій ( по замовчуванням chunk = 1 ) , та нитка , яка закінчує виконання своєї порції ітерацій , отримує першу вільну порцію з chunk ітерацій . Вивільнені нитки отримують нові порції ітерацій до тих пір , поки всі порції не будуть вичерпані. Остання порція може містити менше ітерацій , ніж всі інші. Guided - динамічний розподіл ітерацій , при якому розмір порції зменшується з деякого початкового значення до величини chunk (за замовчуванням chunk = 1 ), яка пропорційна кількості ще не розподілених ітерацій / на кількість ниток , що виконують цикл . Розмір початкового блоку залежить від реалізації . У ряді випадків такий розподіл дозволяє акуратніше розділити роботу і збалансувати завантаження ниток. Кількість ітерацій в останній порції може виявитися менше значення chunk . Auto - спосіб розподілу ітерацій вибирається компілятором та / або системою виконання . Параметр chunk при цьому не задається . Runtime - спосіб розподілу ітерацій вибирається під час роботи програми за значенням змінної середовища OMP_SCHEDULE . параметр chunk при цьому не задається . Приклад 19 демонструє використання опції schedule з параметрами (static), (static , 1), (static , 2), (dynamic), (dynamic, 2), (guided), (guided, 2). У паралельній області виконується цикл, ітерації якого будуть розподілені між існуючими нитками. На кожній ітерації буде надруковано , яка нитка виконала дану ітерацію . У тіло циклу вставлена також затримка , що імітує деякі обчислення . # include # include int main ( int argc , char * argv []) { int i ; # pragma omp parallel private ( i ) { # pragma omp for schedule ( static ) / / # pragma omp for schedule ( static , 1 ) / / # pragma omp for schedule ( static , 2 )
/ / # pragma omp for schedule ( dynamic ) / / # pragma omp for schedule ( dynamic , 2 ) / / # pragma omp for schedule ( guided ) / / # pragma omp for schedule ( guided , 2 ) for ( i = 0 ; i < 10 ; i + +) { printf ( " Нитка % d виконала ітерацію % d \ n " , omp_get_thread_num ( ) , i ) ; sleep ( 1 ) ; } } } Результати виконання прикладу 19 з різними типами розподілу ітерацій наведені в таблиці. Стовпці відповідають різним типам розподілів , а рядки - номеру ітерації. В комірках таблиці вказані номери нитки , що виконувала відповідну ітерацію . У всіх випадках для виконання паралельного циклу використовувалося 4 нитки. для динамічних способів розподілу ітерацій ( dynamic , guided ) конкретний розподіл між нитками може відрізнятися від запуску до запуску.
У таблиці 1 видно різницю між розподілом ітерацій при використанні різних варіантів . До найбільшому дисбалансу привели варіанти розподілу ( static , 2 ) , ( dynamic , 2 ) і ( guided , 2 ) . У всіх цих випадках одній з ниток дістається на дві ітерації більше , ніж іншим. У інших випадках ця різниця кілька згладжується. Приклад 20 демонструє використання опції schedule з параметрами ( static , 6 ) , ( dynamic , 6 ) , ( guided , 6 ) . У паралельній області виконується цикл , ітерації якого будуть розподілені між існуючими нитками. На кожній ітерації буде надруковано , яка нитка виконала дану ітерацію . У тіло циклу вставлена також затримка , що імітує деякі обчислення . # include
# include int main ( int argc , char * argv []) { int i ; # pragma omp parallel private ( i ) { # pragma omp for schedule ( static , 6 ) / / # pragma omp for schedule ( dynamic , 6 ) / / # pragma omp for schedule ( guided , 6 ) for ( i = 0 ; i < 200 ; i + +) { printf ( " Нитка % d виконала ітерацію % d \ n " , omp_get_thread_num ( ) , i ) ; sleep ( 1 ) ; } } } В результаті виконання прикладу 20 з трьома різними варіантами директиви for виходять наступні розподілу ітерацій (рис. 1a - рис . 1c ) .
Ріс.1a . Розподіл ітерацій по нитках для ( static , 6 )
Ріс.1b . Розподіл ітерацій по нитках для ( dynamic , 6 )
Ріс.1c . Розподіл ітерацій по нитках для ( guided , 6 ) На малюнках видно регулярність розподілення порцій по 6 ітерацій при вказівці ( static , 6 ) , більш динамічна картина розподілу таких же порцій при вказівці ( dynamic , 6 ) і розподіл зменшуються порціями при вказівці ( guided , 6 ) . В останньому випадку розмір порцій зменшувався з 24 на самому початку циклу до 6 в кінці. Значення за замовчуванням змінної OMP_SCHEDULE залежить від реалізації . Якщо змінна задана неправильно , то поведінка програми при заданні опції runtime також залежить від реалізації . Задати значення змінної OMP_SCHEDULE можна за допомогою команди такого вигляду: export OMP_SCHEDULE=" dynamic , 1 " Змінити значення змінної OMP_SCHEDULE з програми можна за допомогою виклику функції omp_set_schedule ( ) . Сі :
void omp_set_schedule ( omp_sched_t type , int chunk ) ; Допустимі значення констант описані у файлі omp.h ( omp_lib.h ) . як мінімум , вони повинні включати для мови Сі наступні варіанти : typedef enum omp_sched_t { omp_sched_static = 1 , omp_sched_dynamic = 2 , omp_sched_guided = 3 , omp_sched_auto = 4 } Omp_sched_t ;
За допомогою виклику функції omp_get_schedule ( ) користувач може дізнатися поточне значення змінної OMP_SCHEDULE . Сі : void omp_get_schedule ( omp_sched_t * type , int * chunk ) ; При розпаралеленні циклу програміст повинен переконатися в тому , що ітерації даного циклу не мають інформаційних залежностей . Якщо цикл не містить залежностей , його ітерації можна виконувати в будь-якому порядку , в тому числі паралельно. Дотримання цієї важливої вимоги компілятор не перевіряє , вся відповідальність лежить на програмістові . Якщо дати вказівку компілятору розпаралелити цикл , що містить залежності , компілятор це зробить , але результат роботи програми може виявитися некоректним. 1.2 Завдання для самостійної роботи 1.3 Контрольні питання
Конструкція розподілу виконання роботи ділить виконання коду області між групою потоків яка дійшла до цієї частини коду. Конструкції розподілу виконання роботи не створюють нових потоків Немає уявного бар'єру на вхід у конструкцію з розподілом виконання роботи, однак він є в кінці цієї конструкції. Види конструкції з розподілом виконання роботи: Примітка: конструкція workshare у Фортран тут не продемонстрована але обговорюється пізніше.
DO / for - ділить ітерації циклу між групою потоків. Визначає тип "паралелізм даних".
SECTIONS - ділить роботу на окремі дискретні секції. Кожна секція виконується потоком. Використовується для реалізації типу "функціональний паралелізм".
SINGLE - виконує секцію коду послідовно
Обмеження: Конструкція з розподілом роботи повинна бути динамічно вкладена в паралельній області в порядку директив для виконання паралельно. Конструкції розподілу роботи повинні виконуватись всіма потоками в групі або не виконуватись зовсім Послідовні конструкції з виконання роботи повинні виконуватись в тому ж порядку всіма потоками групи
паралельні секції Директива sections ( sections ... end sections ) використовується для завдання кінцевого ( неітеративного ) паралелізму . Сі : # pragma omp sections * опція * *,+ опція + ...+
Ця директива визначає набір незалежних секцій коду , кожна з яких виконується своєї ниткою. Незалежні директиви SECTION є вкладеними в директиву SECTIONS. Кожна SECTION виконується один раз потоком групи. Різні секції можуть виконуватись різними потоками. Можливе виконання і більше ніж однієї секції на потоці, якщо вона достатньо швидко виконується, але це залежить від реалізації. Можливі опції : ? private (список; ? firstprivate (список); ? lastprivate (список) - Значення змінної копіюється в оригінальний об'єкт змінної з останньої ітерації або секції оточуючої конструкції. Наприклад, один з групи потоків що виконує останню ітерацію конструкції DO, або потік, що виконує останню SECTION в конструкції SECTIONS використовує копію з її власним значенням. Пункт LASTPRIVATE комбінує поведінку пункту PRIVATE з копією змінної з останньої операції циклу або секції, до оригінального об'єкту змінної. ? reduction (оператор : список ); ? nowait - наприкінці блоку секцій відбувається неявна бар'єрна синхронізація паралельно працюючих ниток : їх подальше виконання відбувається тільки тоді , коли всі вони досягнуть даної точки ; якщо в подібній затримці немає необхідності , опція nowait дозволяє ниткам , вже дійшли до кінця своїх секцій , продовжити виконання без синхронізації з іншими. Директива section задає ділянку коду усередині секції sections для виконання однієї ниткою. Сі : # pragma omp section Перед першою ділянкою коду в блоці sections директива section не обов'язкова. Директива SECTION повинна бути вкладена в область яку оточує директива SECTIONS (тобто SECTION не може бути "осиротілою" ). не специфікується, які саме нитки будуть задіяні для виконання якої секції. Якщо кількість ниток більше кількості секцій , то частина ниток для виконання даного блоку секцій не буде задіяно . Якщо кількість ниток менше кількості секцій , то деяким (або усім ) ниткам дістанеться більше однієї секції. Приклад 21 ілюструє застосування директиви sections . Спочатку три нитки , на які розподілилися три секції section , виведуть повідомлення зі своїм номером , а потім всі нитки надрукують однакове повідомлення зі своїм номером. # include # include int main ( int argc , char * argv []) { int n ; # pragma omp parallel private ( n ) { n = omp_get_thread_num (); # pragma omp sections { # pragma omp section { printf ( "Перша секція , процес % d \ n " , n ) ; }
# pragma omp section { printf ( " Друга секція , процес % d \ n " , n ) ; } # pragma omp section { printf ( "Третя секція , процес % d \ n " , n ) ; } } printf ( " Паралельна область , процес % d \ n " , n ) ; } } Приклад 21a . Директива sections на мові Сі.
lastprivate Приклад 22 демонструє використання опції lastprivate . У даному прикладі опція lastprivate використовується разом з директивою sections . змінна n оголошена як lastprivate змінна. Три нитки ,які виконують секції section , привласнюють своїй локальній копії n різні значення. По виході з області sections значення n з останньої секції присвоюється локальним копіям у всіх нитках , тому всі нитки надрукують число 3 . Це ж значення збережеться для змінної n і в послідовній області.
# include # include int main ( int argc , char * argv []) { int n = 0 ; # pragma omp parallel { # pragma omp sections lastprivate ( n ) { # pragma omp section { n=1; } # pragma omp section { n=2; } # pragma omp section { n=3; } } printf ( " Значення n на нитки % d : % d \ n " , omp_get_thread_num ( ) , n ) ; } printf ( " Значення n в послідовній області: % d \ n " , n ) ; } Приклад 22a . Опція lastprivate на мові Сі.
Завдання ( tasks ) Директива task ( task ... end task ) застосовується для виділення окремого незалежної завдання. Сі : # pragma omp task * опція * *,+ опція + ...+ Поточна нитка виділяє як завдання асоційований з директивою блок операторів. Завдання може виконуватися негайно після створення або бути відкладеної на невизначений час і виконуватися по частинах. Розмір таких частин , а також порядок виконання частин різних відкладених завдань визначається реалізацією . Можливі опції : ? if (умова) - породження нового завдання тільки при виконанні деякої умови ; якщо умова не виконується , то завдання буде виконано поточної ниткою і негайно ; ? untied - опція означає , що у разі відкладання завдання може бути продовжена будь ниткою з тих, що виконують дану паралельну область; якщо опція не вказана , то завдання може бути продовжено тільки ниткою, яка породила її; ? default ( private | firstprivate | shared | none ) - всім змінним в завданню , яким явно не призначений клас , буде призначений клас private , firstprivate або shared відповідно; none означає , що всім змінним в задачі клас повинен бути призначений ; ? private (список ) ; ? firstprivate (список ); ? shared (список). Для гарантованого завершення в точці виклику всіх запущених завдань використовується директива taskwait . Сі : # pragma omp taskwait Нитка , що виконала дану директиву , зупиняється до тих пір, поки не будуть завершені всі раніше запущені даної ниткою незалежні завдання.
Завдання 2 · Чи можуть функції omp_get_thread_num ( ) і omp_get_num_threads ( ) повернути однакові значення на кількох нитках однієї паралельної області ? · Чи можна розподілити між нитками ітерації циклу без використання директиви for ( do ... [ end do ])? · Чи можна однією директивою розподілити між нитками ітерації відразу декількох циклів ? · Чи можливо, що при статичному розподілі ітерацій циклу ниткам дістанеться різну кількість ітерацій ? · Чи можуть при повторному запуску програми ітерації распределяемого циклу дістатися іншим ниткам ? Якщо так , то за яких способах розподілу ітерацій ? · Для чого може бути корисно вказувати параметр chunk при способі розподілу ітерацій guided ?
· Чи можна реалізувати паралельні секції без використання директив sections ( sections ... end sections ) і section ? · Як при виході з паралельних секцій розіслати значення деякої локальної змінної всім ниткам , виконуючим дану паралельну область ? · В яких випадках може стати в нагоді механізм завдань? · Напишіть паралельну програму, що реалізовує скалярний добуток двох векторів . · Напишіть паралельну програму, що реалізовує пошук максимального значення вектора .
Синхронізація Цілий набір директив у OpenMP призначений для синхронізації роботи ниток .
Бар'єр Найпоширеніший спосіб синхронізації в OpenMP - бар'єр. Він оформляється за допомогою директиви barrier . Сі : #pragma omp barrier
newline
Нитки ,які виконують поточну паралельну область , дійшовши до цієї директиви , зупиняються і чекають , поки всі нитки не дійдуть до цієї точки програми , після чого розблокуються і продовжують працювати далі. Всі потоки групи (або жоден) повинні виконати вміст області BARRIER. Крім того , для розблокування необхідно, щоб всі синхронізуються нитки завершили всі породжені ними завдання ( task ) . Послідовність областей розподілу роботи і областей бар'єрів повинна бути однаковою для всіх потоків в групі. Приклад 24 демонструє застосування директиви barrier . Директива бар'єр використовується для упорядкування виведення від працюючих ниток. Видачі з різних ниток " Повідомлення 1 " і " Повідомлення 2 " можуть перемежовуватися в довільному порядку , а видача " Повідомлення 3 " з усіх ниток прийде строго після двох попередніх видач . # include # include int main ( int argc , char * argv []) { # pragma omp parallel { printf ( " Повідомлення 1 \ n " ) ; printf ( " Повідомлення 2 \ n " ) ; # pragma omp barrier printf ( " Повідомлення 3 \ n " ) ;
} } Приклад 24a . Директива barrier на мові Сі.
Директива ordered Директиви ordered ( ordered ... end ordered ) визначають блок всередині тіла циклу , який повинен виконуватися в тому порядку , в якому ітерації йдуть в послідовному циклі. Сі : #pragma omp for ordered [clauses...] (loop region) #pragma omp ordered
newline
structured_block (endo of loop region)
Блок операторів відноситься до самого внутрішнього з осяжних циклів , а в паралельному циклі повинна бути задана опція ordered . Нитка , що виконує перший ітерацію циклу , виконує операції даного блоку. Нитка , що виконує будь-яку наступну ітерацію , повинна спочатку дочекатися виконання всіх операцій блоку всіма нитками , що виконують попередні ітерації. Може використовуватися , наприклад , для впорядкування виводу від паралельних ниток. Директива ORDERED може вказуватись лише в динамічних масштабах for чи parallel for (C/C++). При використанні директиви дозволений лише один потік в будь-який момент часу. Ітерація циклу не повинна виконувати одну й ту ж директиву ORDERED більше чим раз, і не повинна виконувати більше одної директиви ORDERED. Цикл що містить директиву ORDERED повинен бути циклом з використанням пункту ORDERED. Приклад 25 ілюструє застосування директиви ordered та опції ordered . Цикл for ( do ) позначений як ordered . Усередині тіла циклу йдуть дві видачі - одна поза блоком ordered , а друга - всередині нього. В результаті першого видача виходить невпорядкованою , а друга йде в строгому порядку по зростанню номера ітерації.
# include # include int main ( int argc , char * argv []) { int i , n ; # pragma omp parallel private ( i , n ) { n = omp_get_thread_num ();
# pragma omp for ordered for ( i = 0 ; i < 5 ; i + +) { printf ( " Нитка % d , ітерація % d \ n " , n , i ) ; # pragma omp ordered { printf ( " ordered : Нитка % d , ітерація % d \ n " , n , i ) ; } } } } Приклад 25a . Директива ordered і опція ordered на мові Сі.
критичні секції За допомогою директив critical оформляється критична секція програми . Сі : #pragma omp critical [ name ] structured_block
newline
У кожен момент часу в критичній секції може перебувати не більше однієї нитки . Якщо критична секція вже виконується якою-небудь ниткою , то всі інші нитки , які виконали директиву для секції з даним ім'ям , будуть заблоковані , поки нитка, що увійшла, не закінчить виконання даної критичної секції. Як тільки нитка вийде з критичної секції , одна із заблокованих на вході ниток увійде в неї. Якщо на вході в критичну секцію стояло кілька ниток , то випадковим чином вибирається одна з них , а інші заблоковані нитки продовжують очікування . Всі неіменовані критичні секції умовно асоціюються з одним і тим же ім'ям. Всі критичні секції , що мають одне і теж ім'я , розглядаються єдиної секцією , навіть якщо знаходяться в різних паралельних областях (тобто діють як глобальні ідентифікатори). Побічні входи і виходи з критичної секції заборонені. Приклад 26 ілюструє застосування директиви critical . Мінлива н оголошена поза паралельної області , тому за замовчуванням є спільною . Критична секція дозволяє розмежувати доступ до змінної n . Кожна нитка по черзі присвоїть n свій номер і потім надрукує отримане значення . # include # include int main ( int argc , char * argv []) { int n ;
# pragma omp parallel { # pragma omp critical { n = omp_get_thread_num (); printf ( " Нитка % d \ n " , n ) ; } } }
Якби в прикладі 26 не була зазначена директива critical , результат виконання програми був би непередбачуваний. З директивою critical порядок виводу результатів може бути довільним , але це завжди буде набір одних і тих же чисел від 0 до OMP_NUM_THREADS - 1 . Звичайно , подібного ж результату можна було б досягти іншими способами , наприклад , оголосивши змінну n локальною , тоді кожна нитка працювала б зі своєю копією цієї змінної . Однак у виконанні цих фрагментів різниця суттєва . Якщо є критична секція , то в кожен момент часу фрагмент буде оброблятися лише якою-небудь однією ниткою. Решта нитки , навіть якщо вони вже підійшли до даної точки програми і готові до роботи , чекатимуть своєї черги. Якщо критичної секції немає , то всі нитки можуть одночасно виконати дану ділянку коду. З одного боку , критичні секції надають зручний механізм для роботи із загальними змінними. Але з іншого боку , користуватися ним потрібно обачно , оскільки критичні секції додають послідовні ділянки коду в паралельну програму , що може знизити її ефективність .
Директива atomic Частим випадком використання критичних секцій на практиці є оновлення загальних змінних. Наприклад , якщо змінна sum є загальною і оператор виду sum = sum + expr знаходиться в паралельній області програми , то при одночасному виконанні даного оператора декількома нитками можна отримати некоректний результат. Щоб уникнути такої ситуації можна скористатися механізмом критичних секцій або спецiального для таких випадків директивою atomic . Сі : #pragma omp atomic newline statement_expression
Дана директива відноситься до оператора привласнення, що йде безпосередньо за нею ( на конструкції, які в ньому використовувані накладаються досить зрозумілі обмеження) , гарантуючи коректну роботу із загальною змінною, що стоїть в його лівій частині . На час виконання оператора блокується доступ до даної змінної всім запущеним в даний момент ниткам , крім нитки , що виконує операцію. Атомарною є тільки робота з змінної в лівій частині оператора привласнення , при цьому обчислення в правій частині не зобов'язані бути атомарними .
Приклад 27 ілюструє застосування директиви atomic . У даному прикладі проводиться підрахунок загальної кількості породжених ниток. Для цього кожна нитка збільшує на одиницю значення змінної count . Для того , щоб запобігти одночасна зміна декількома нитками значення змінної, що стоїть в лівій частині оператора привласнення , використовується директива atomic . # include # include int main ( int argc , char * argv []) { int count = 0 ; # pragma omp parallel { # pragma omp atomic count + + ; } printf ( " Число ниток : % d \ n " , count ) ; } Приклад 27a . Директива atomic на мові Сі.
Директива TASKWAIT Директива TASKWAIT визначає очікування завершення завдань нащадків, що були створені від початку виконання даного завдання. C/C++ #pragma omp taskwait
newline
Обмеження: Існує кілька обмежень щодо використання taskwait в програмі, оскільки його конструкція не передбачена С-дібним синтаксисом. Дана директива може бути розміщена тільки в точках коду, де дозволені загальні конструкції мови. Директива не може використовуватись після використання операторів if, while, do, switch, чи label.
Замки Один з варіантів синхронізації в OpenMP реалізується через механізм замків ( locks ) . В якості замків використовуються загальні цілочисельні змінні ( розмір повинен бути достатнім для зберігання адреси ) . Дані змінні повинні використовуватися тільки як параметри примітивів синхронізації. Замок може знаходитися в одному з трьох станів: неініціалізованих , розблокований або заблокований . Розблокований замок може бути захоплений деякої ниткою. При цьому він переходить в заблокований стан . Нитка , що захопила замок , і тільки вона може його звільнити , після чого замок повертається в розблокувала стан . Є два типи замків : прості замки і множинні замки. Множинний замок може багаторазово захоплюватися однією ниткою перед його звільненням , в той час як простий замок може бути захоплений тільки одного разу . Для множинного замку вводиться поняття коефіцієнта захопленості ( nesting count ) . Спочатку він
встановлюється в нуль , при кожному наступному захоплюванні збільшується на одиницю , а при кожному звільнення зменшується на одиницю. Множинний замок вважається розблокованим , якщо його коефіцієнт захопленості дорівнює нулю. Для ініціалізації простого або множинного замку використовуються відповідно функції omp_init_lock ( ) і omp_init_nest_lock ( ) . Сі : void omp_init_lock(omp_lock_t *lock) void omp_init_nest_lock(omp_nest_lock_t *lock)
Після виконання функції замок переводиться в розблокуваний стан . Для множинного замку коефіцієнт захоплення встановлюється в нуль. Функції omp_destroy_lock ( ) і omp_destroy_nest_lock ( ) використовуються для переведення простого або множинного замка в неініціалізований стан .
Сі : void omp_destroy_lock(omp_lock_t *lock) void omp_destroy_nest_lock(omp_nest_lock_t *lock)
Для захоплювання замка використовуються функції omp_set_lock ( ) і omp_set_nest_lock ( ) . Сі : void omp_set_lock(omp_lock_t *lock) void omp_set_nest__lock(omp_nest_lock_t *lock)
Нитка, що викликала цю функцію чекає звільнення замка , а потім захоплює його . Замок при цьому переключається в заблокований стан . Якщо множинний замок вже захоплений даною ниткою , то нитка не блокується , а коефіцієнт захопленості збільшується на одиницю. Для звільнення замка використовуються функції omp_unset_lock ( ) і omp_unset_nest_lock ( ) . Сі : void omp_unset_lock(omp_lock_t *lock) void omp_unset_nest__lock(omp_nest_lock_t *lock)
Виклик цієї функції звільняє простий замок , якщо він був захоплений ниткою, що його викликала. Для множинного замка зменшує на одиницю коефіцієнт захоплення . Якщо коефіцієнт стане рівний нулю , замок звільняється . Якщо після звільнення замка є нитки , заблоковані на операції захоплення цього замка , він буде відразу ж захоплений однією з чекаючих ниток.
Приклад 28 ілюструє застосування технології замків. Змінна lock використовується для блокування. У послідовній області ініціалізується ця змінна за допомогою функції omp_init_lock() . На початку паралельної області кожна нитка привласнює змінній n свій порядковий номер . Після цього за допомогою функції omp_set_lock ( ) одна з ниток виставляє блокування , а інші нитки чекають ,
поки нитка , що викликала цю функцію, не зніме блокування за допомогою функції omp_unset_lock ( ) . Всі нитки по черзі виведуть повідомлення " Початок закритої секції ... " і "Кінець закритої секції ..." , при цьому між двома повідомленнями від однієї нитки не можуть зустрітися повідомлення від іншої нитки. Наприкінці за допомогою функції omp_destroy_lock ( ) відбувається звільнення змінної lock . # include # include omp_lock_t lock ; int main ( int argc , char * argv []) { int n ; omp_init_lock ( & lock ) ; # pragma omp parallel private ( n ) { n = omp_get_thread_num (); omp_set_lock ( & lock ) ; printf ( " Початок закритою секції , нитка % d \ n " , n ) ; sleep ( 5 ) ; printf ( "Кінець закритою секції , нитка % d \ n " , n ) ; omp_unset_lock ( & lock ) ; } omp_destroy_lock ( & lock ) ; } Приклад 28a . Використання замків на мові Сі.
Для неблокуючий спроби захоплення замку використовуються функції омп_test_lock ( ) і omp_test_nest_lock ( ) . Сі : int omp_test_lock(omp_lock_t *lock) int omp_test_nest__lock(omp_nest_lock_t *lock)
Дана функція пробує захопити вказаний замок. Якщо це вдалося , то для простого замка функція повертає 1 , а для множинного замка - новий коефіцієнт захоплення . Якщо замок захопити не вдалося , в обох випадках повертається 0. Приклад 29 ілюструє застосування технології замків і використання функції omp_test_lock ( ) . У даному прикладі змінна lock використовується для блокування. На початку проводиться
ініціалізація даної змінної за допомогою функції omp_init_lock ( ) . У паралельній області кожна нитка привласнює змінної n свій порядковий номер . Після цього за допомогою функції omp_test_lock ( ) нитки спробують виставити блокування. Одна з ниток успішно виставить блокування , інші ж нитки надрукують повідомлення " Секція закрита ... " , призупинять роботу на дві секунди з допомогою функції sleep ( ) , а після знову намагатимуться встановити блокування . Нитка , яка встановила блокування , повинна зняти її за допомогою функції omp_unset_lock ( ) . Таким чином , код, що знаходиться між функціями установки і зняття блокування , буде виконаний кожної ниткою по черзі. В даному випадку , всі нитки по черзі виведуть повідомлення " Початок закритою секції ... "і " Кінець закритою секції ... " , але при цьому між двома повідомленнями від однієї нитки можуть зустрітися повідомлення від інших ниток про невдалу спробу увійти в закриту секцію. Наприкінці за допомогою функції omp_destroy_lock ( ) відбувається звільнення змінної lock . # include # include int main ( int argc , char * argv []) { omp_lock_t lock ; int n ; omp_init_lock ( & lock ) ; # pragma omp parallel private ( n ) { n = omp_get_thread_num (); while (! omp_test_lock ( & lock )) { printf ( " Секція закрита , нитка % d \ n " , n ) ; sleep ( 2 ) ; } printf ( " Початок закритою секції , нитка % d \ n " , n ) ; sleep ( 5 ) ; printf ( "Кінець закритою секції , нитка % d \ n " , n ) ; omp_unset_lock ( & lock ) ; } omp_destroy_lock ( & lock ) ; } Приклад 29a . Функція omp_test_lock ( ) на мові Сі.
Використання замків є найбільш гнучким механізмом синхронізації , оскільки за допомогою замків можна реалізувати всі інші варіанти синхронізації.
Директива flush Оскільки в сучасних паралельних обчислювальних системах може використовуватися складна структура та ієрархія пам'яті , користувач повинен мати гарантії того , що в необхідні йому моменти часу всі нитки будуть бачити єдиний узгоджений образ пам'яті. Саме для цих цілей і призначена директива flush . Сі : #pragma omp flush (list)
newline
Виконання даної директиви передбачає , що значення всіх змінних (або змінних зі списку , якщо він заданий ) , тимчасово зберігаються в регістрах і кеш-пам'яті поточної нитки , будуть занесені в основну пам'ять ; всі зміни змінних , зроблені ниткою під час роботи , стануть видимі іншим ниткам ; якщо якась інформація зберігається в буферах виведення , то буфери будуть скинуті і т.п. При цьому операція проводиться тільки з даними, які викликала нитка , дані , що змінювалися іншими нитками , не зачіпаються. Оскільки виконання директиви в повному обсязі може спричинити значні накладні витрати , а в потрібна гарантія узгодженого подання не всіх , а лише окремих змінних , то ці змінні можна явно перерахувати в директиві списком . До повного завершення операції ніякі дії з перерахованими в ній змінними не можуть розпочатися. Якщо ви захочете добавити в список вказівник то слід пам'ятати що ігнорується лише вказівник, але не змінна на яку він вказує. Неявно flush без параметрів присутній в директиві barrier , на вході і виході областей дії директив parallel , critical , ordered , на виході областей розподілу робіт (for, sections, single) , якщо не використовується опція nowait , у викликах функцій omp_set_lock ( ) , omp_unset_lock ( ) , omp_test_lock ( ) , омп_set_nest_lock ( ) , omp_unset_nest_lock ( ) , omp_test_nest_lock ( ) , якщо при цьому замок встановлюється або знімається , а також перед породженням і після завершення будь-якої задачі ( task ) . Крім того , flush викликається для змінної , що бере участь в операції , асоційованої з директивою atomic . Зауважимо , що flush не застосовується на вході області розподілу робіт , а також на вході і виході області дії директиви master .
Завдання 3 · Що станеться , якщо бар'єр зустрінеться не у всіх нитках , виконуючих поточну паралельну область ? · Чи можуть дві нитки одночасно перебувати в різних критичних секціях ? · У чому полягає різниця у використанні критичних секцій і директиви atomic ? · Змоделюйте за допомогою механізму замків: o бар'єрну синхронізацію ; o критичну секцію. · Придумайте приклад на використання множинного замку. · Коли виникає необхідність у використанні директиви flush ?
· Реалізуйте паралельний алгоритм методу Гаусса розв'язання систем лінійних алгебраїчних рівнянь . Виберіть оптимальні варіанти розпаралелювання і проведіть аналіз ефективності реалізації .
Додаткові змінні середовища і функції Змінна OMP_MAX_ACTIVE_LEVELS задає максимально допустиму кількість вкладених паралельних областей. Значення може бути встановлено за допомогою виклику функції omp_set_max_active_levels ( ) . Сі : void omp_set_max_active_levels ( int max ) ; Якщо значення max перевищує максимально допустимий в системі , буде встановлено максимально допустиме в системі значення . При виклику з паралельної області результат виконання залежить від реалізації . Значення змінної OMP_MAX_ACTIVE_LEVELS може бути отримано за допомогою виклику функції omp_get_max_active_levels ( ) . Сі : int omp_get_max_active_levels ( void ) ;
Функція omp_get_level ( ) видає для нитки, яка її викликала, кількість вкладених паралельних областей в даному місці коду. Сі : int omp_get_level ( void ) ; При виклику з послідовної області функція повертає значення 0 . Функція omp_get_ancestor_thread_num() повертає для рівня вкладеності паралельних областей , заданого параметром level , номер нитки ,яка породила цю нитку. Сі : int omp_get_ancestor_thread_num ( int level ) ; Якщо level менше нуля або більше поточного рівня вкладеності , повертається -1 . Якщо level = 0 , функція поверне 0 , а якщо level = omp_get_level ( ) , виклик відповідає виклику функції omp_get_thread_num ( ) . Функція omp_get_team_size () повертає для заданого параметром level рівня вкладеності паралельних областей кількість ниток , породжених однієї батьківської ниткою. Сі : int omp_get_team_size ( int level ) ; Якщо level менше нуля або більше поточного рівня вкладеності , повертається -1 . Якщо level = 0 , функція поверне 1 , а якщо level = omp_get_level ( ) , виклик відповідає виклику функції omp_get_num_threads ( ) . Функція omp_get_active_level () повертає для викликала нитки кількість вкладених паралельних областей , оброблюваних більш ніж однієї ниткою , в даному місці коду. Сі :
int omp_get_active_level ( void ) ; При виклику з послідовної області повертає значення 0 . Змінна середовища OMP_STACKSIZE задає розмір стека для створюваних з програми ниток. Значення змінної може здаватися у вигляді setenv OMP_STACKSIZE 2000500B setenv OMP_STACKSIZE "3000 k " setenv OMP_STACKSIZE 10M setenv OMP_STACKSIZE " 10 M " setenv OMP_STACKSIZE "20 m " setenv OMP_STACKSIZE " 1G" setenv OMP_STACKSIZE 20000 , де size - позитивне ціле число , а букви B , K , M , G задають відповідно , байти , кілобайти , мегабайти і гігабайти . Якщо жодної з цих букв не вказано , розмір задається в кілобайтах. Якщо заданий неправильний формат або неможливо виділити потрбіний розмір стека , результат буде залежати від реалізації . Наприклад , в Linux в командній оболонці bash задати розмір стека можна за допомогою наступної команди : export OMP_STACKSIZE = 2000K
Змінна середовища OMP_WAIT_POLICY забезпечує підказку, яка реалізація OpenMP підтримує бажану поведінку очікування потоку. Сумісна реалізація OpenMP може і не дотримуватися встановленої змінної середовища. Якщо задано значення ACTIVE , то процесу, що чекає, будуть виділятися цикли процесорного часу , а при значенні PASSIVE процес, який чекає, може бути відправлений у сплячий режим , при цьому процесор може бути призначений іншим процесам . Змінна середовища OMP_THREAD_LIMIT задає максимальне число ниток , допустимих в програмі. Якщо значення змінної не є позитивним цілим числом або перевищує максимально допустимий в системі число процесів , поведінка програми буде залежати від реалізації . Значення змінної може бути отримано за допомогою процедури omp_get_thread_limit ( ) . Сі : int omp_get_thread_limit ( void ) ;
приклади програм Приклад 30 реалізує найпростішу програму обчислення числа Пі . Для розпаралелювання досить додати у послідовну програму всього дві стрічки. # include double f ( double y ) { return (4.0 / (1.0 + y * y )) ;} int main ( ) {
double w , x , sum , pi ; int i ; int n = 1000000; w = 1.0 / n ; sum = 0.0 ; # pragma omp parallel for private ( x ) shared ( w ) \ reduction ( + : sum ) for ( i = 0 ; i < n ; i + +) { x = w * ( i -0.5 ) ; sum = sum + f ( x ) ; } pi = w * sum ; printf ( " pi = % f \ n " , pi ) ; } Приклад 30a . Обчислення числа Пі на мові Сі. Приклад 31 реалізує найпростішу програму, що реалізовує множення двох квадратних матриць . У програмі змиритися час на основний обчислювальний блок , що не включає початкову ініціалізацію # include # include # define N 4096 double a [N ] [ N] , b [N ] [ N] , c [N ] [ N] ; int main ( ) { int i , j , k ; double t1 , t2 ; / / Ініціалізація матриць for ( i = 0 ; i
for ( j = 0 ; j
1
2
4
165.442016 114.413227 68.271149
8
39.039399
Таблиця 2 . Часи виконання твору матриць на вузлі суперкомп'ютера СКІФ МГУ « Чебишев ».
Правило вкладання директив Об'єднання директив: Директиви DO/for, SECTIONS, SINGLE, MASTER і BARRIER динамічно приєднюються до директиви PARALLEL, якщо остання існує. Якщо відсутня область паралельного виконання, то ці директиви на даватимуть ніякого результату. Директива ORDERED динамічно приєднюється до оточуючої її директиви DO/for. Директива ATOMIC отримує доступ не тільки до потоків поточної групи, але й до всіх інших, які розуміють її вказівки. Директива СRITICAl отримує доступ не тільки до потоків поточної групи, але й до всіх інших, які розуміють її вказівки. Директива не може об'єднюватись з сусідніми паралельними областями PARALELL, що знаходяться навколо поточної директиви PARALELL. Вкладення директив: Область worksharing не може бути вкладена в області worksharing, explicit task, critical, ordered, atomic, чи master region. Область barrier не може бути вкладена в області worksharing, explicit task, critical, ordered, atomic, чи master region. Область master не може бути вкладена в області worksharing, atomic, чи explicit task region. Область ordered не може бути вкладена в області critical, atomic, чи explicit task region. Область ordered повинна бути вкладена в область loop (або паралельний loop) з відповідним пунктом. Область critical не може бути вкладена в область critical з таким самим ім'ям. Зверніть увагу, що це обмеження не є достатнім для запобігання "мертвих" замикань. Області parallel, flush, critical, atomic, taskyield, і explicit не можуть бути вкладені в область atomic.