Kurs jest częścią programu Xtreme Computing.

Autorem kursu jest Paweł Przybyłowicz - asystent na Wydziale Matematyki Stosowanej AGH.

Krok 7 - dyrektywy barier i nowait

W standardzie OpenMP na końcu każdego bloku kodu wyróżnionego dyrektywami parallel, sections, single czy też for, umieszczana jest przez kompilator tzw. bariera (barrier). Wątek natrafiając na to miejsce zostaje wstrzymany i będzie czekać na pozostałe, dopóki każdy z wątków nie dotrze do bariery. Na przykład: dany wątek po zakończeniu swojego zadania w pętli for czeka aż pozostałe wątki zakończą wykonywanie przypisanych im iteracji w tej pętli. Dopiero gdy wszystkie wątki wykonają w pętli for swoją pracę, wtedy przechodzą do wykonywania dalszej części programu. Jest to więc pewien punkt zbiorczy dla wątków.

Programista ma też możliwość ręcznego umieszczania bariery pisząc:

#pragma omp barrier

Używając tej dyrektywy należy pamiętać, że bariera zawsze musi być wykryta przez wszystkie wątki uczestniczące w zrównoleglaniu danego bloku programu. Inaczej pewna grupa wątków może oczekiwać w nieskończoność na resztę wątków, które nie wykryły bariery. Standard C/C++ narzuca również dodatkowe ograniczenie na sposób używania dyrektywy barrier. Może być ona umieszczona tylko w takim miejscu programu, aby jej ewentualne wykasowanie nie powodowało błędu składniowego.

Poniżej pokazujemy przykład użycia dyrektywy barrier w programie.

Program 07/01 - step_0701.c

#include <stdio.h> #include <stdlib.h> #include <time.h> #include <omp.h> #define MAX_NT 4 void thread_work(int id) { clock_t t1, t2; printf("Watek %d zaczyna prace...\n", id - 1); t1 = clock(); do { t2 = clock(); } while ((((double)t2 - t1) / CLOCKS_PER_SEC) < id); } int main(void) { int id; #pragma omp parallel num_threads(MAX_NT) private(id) { id = omp_get_thread_num(); thread_work(id + 1); printf("Watek %d zakonczyl prace i czeka przy barierze...\n", id); #pragma omp barrier printf("Watek %d juz poza bariera.\n", id); } printf("Nacisnij dowolny klawisz...."); getchar(); return 0; }

W powyższym programie każdy z czterech wątków wywołuje funkcję thread_work. Zadaniem tej procedury jest zapętlenie wątku na podaną liczbę sekund. W programie przyjęliśmy długości przedziałów czasowych uśpienia wątków na 1, 2, 3 i 4 sekund aby po uruchomieniu użytkownik miał czas zobaczyć w jakiej kolejności poszczególne wątki kończą swoją pracę i w jakiej kolejności pojawiają się przy ustawionej w programie barierze. Gdy wszystkie wątki już do niej dotrą przechodzą do kolejnej części programu i wysyłają, w związku z tym, stosowny komunikat.

Taki sposób wykorzystania mechanizmu bariery jest najbardziej naturalny. Każdemu z wątków zlecamy bowiem wykonanie pewnego mniejszego zadania, które jest częścią pewnej większej całości jak np. w metodach typu dziel-i-rządź. W takich programach rozdzielamy zadanie pomiędzy dostępne wątki, ustawiamy barierę (lub kompilator sam umieszcza ją w sposób niejawny) a następnie łączymy ze sobą uzyskane wyniki. Ustawienie bariery zapewnia, że wyniki zostaną użyte dopiero wówczas gdy każdy z wątków skończy swoje obliczenia. W sposób niejawny taka bariera została umieszczona na przykład w programie sortującym z Lekcji 6 po nawiasach klamrowych sekcji parallel i przed wywołaniem funkcji

// ... min_sub_tab(min, 0, n_threads, &global_min); // ...

Każdy wątek musiał w tym programie znaleźć minimum w swojej części tablicy i czekać na pozostałe aż zrobią to samo. Dopiero później można poszukiwać minimum pomiędzy wynikami cząstkowymi. Brak bariery mógłby spowodować błędne działanie programu. Szukane było by bowiem minimum tablicy min, która nie zawiera wyników obliczeń każdego z wątków.

Nie zawsze jednak umieszczenie bariery jest rozwiązaniem optymalnym. Czasami jej pominięcie wpłynęło by znacznie na poprawę wydajność programu. Klauzula nowait pozwala skasować barierę, która jest umieszczana domyślnie przez kompilator po dyrektywach for czy też sections. Należy przy tym pamiętać, że bariera występująca na końcu bloku zrównoleglanego parallel nie może być za pomocą tej klauzuli zlikwidowana i nie możemy napisać

// Niepoprawne uzycie klauzuli 'nowait' #pragma omp parallel nowait { // ... } // W tym miejscu zawsze znajduje się niejawna bariera // ...

lub

// Niepoprawne uzycie klauzuli 'nowait' #pragma omp parallel for nowait { // ... } // W tym miejscu zawsze znajduje się niejawna bariera // ...

W powyższych przypadkach kompilator zasygnalizuje błąd.

W przypadku dyrektyw for i sections poprawne użycie opcji nowait wygląda następująco:

// ... #pragma omp parallel { #pragma omp for nowait for (...) { } // W tym miejscu nie ma już bariery // ... } // ...

czy też

// ... #pragma omp parallel { #pragma omp sections nowait { } // W tym miejscu nie ma już bariery // ... } // ...

Po usunięciu bariery, za pomocą klauzuli nowait, dany wątek kończąc swoje zadanie czy to w bloku sections, czy for przechodzi natychmiast do kolejnego. Spójrzmy na poniższy program.

Program 07/02 - step_0702.c

#include <stdio.h> #include <stdlib.h> #include <time.h> #include <omp.h> void wait(int sec) { clock_t t1, t2; t1 = clock(); do { t2 = clock(); } while ((((double)t2 - t1) / CLOCKS_PER_SEC) < sec); } int main(void) { long n = 10, i; #pragma omp parallel num_threads(4) default(none) shared(n) { #pragma omp sections nowait { #pragma omp section { printf("Sections - Watek %d wykonuje zadanie...\n", omp_get_thread_num()); wait(2); } #pragma omp section { printf("Sections - Watek %d wykonuje zadanie...\n", omp_get_thread_num()); wait(2); } } #pragma omp for schedule(dynamic) private(i) for (i = 0; i < n; i++) { printf("Iteracje %d wykonuje watek %d.\n", i, omp_get_thread_num()); wait(2); } } printf("Nacisnij dowolny klawisz..."); getchar(); return 0; }

W sekcji parallel tworzone są cztery wątki. Dwa z nich są przypisane do wykonywania zadań w bloku sections. Jeśli usunęlibyśmy klauzulę nowait wówczas pozostałe dwa wątki czekały by bezczynnie aż pierwsze dwa zakończą wykonywać zlecone im działania. Jeśli zadanie znajdujące się po bloku wyróżnionym dyrektywą sections jest niezależne od wyników zadań objętych konstrukcjami section, wówczas można zastosować opcje nowait i wtedy dwa wątki, które są wolne, od razu przechodzą do wykonywania pętli for. Gdy pierwsze dwa wątki zakończą obliczenia w blokach section również do nich dołączają i cała czwórka uczestniczy w równoległym wykonywaniu iteracji pętli.

Można zapytać co się stanie, gdy zaraz po bloku sections, a przed zrównolegleniem pętli for dopiszemy jawnie dyrektywę barrier? Jak się wtedy program będzie zachowywał? Okazuje się, że dyrektywa barrier stoi wyżej w hierarchii niż klauzula nowait tzn. jeśli napiszemy:

// ... #pragma omp parallel num_threads(4) default(none) shared(n) { #pragma omp sections nowait { // ... } } // Jawnie wymuszamy barierę #pragma omp barrier #pragma omp for schedule (dynamic) private (i) for (i = 0; i < n; i++) { // ... } // ...

wówczas opcja nowait nie kasuje jawnie postawionej bariery. Dwa wątki będą wykonywać przypisane im zadania w bloku sections, a pozostałe dwa będą bezczynnie czekać aż tamte skończą pracę. Tak więc jeśli chcemy gdzieś w programie wymusić umieszczenie bariery, wówczas lepiej zrobić to ręcznie, a nie czekać aż zrobi to za nas kompilator.

Bardzo ważna, przy używaniu opcji nowait, jest podkreślona wyżej niezależność wykonywanych bloków programu. Jeśli brak jest niezależności obliczeń, wówczas nieopatrzne użycie klauzuli nowait może prowadzić do błędów. Niepoprawne wyniki da przykładowo poniższa konstrukcja.

// ... #pragma omp parallel num_threads(2) default(none) shared(a, b, c, d, sum) { #pragma omp sections nowait { #pragma omp section { task1(a, &b); } #pragma omp section { task2(b, &c); } } if (omp_get_thread_num() == 0) sum = b + c; } // ...

Powyżej przypisaliśmy dwóm wątkom pewne zadania task1 i task2. Zakładamy, że każda z tych funkcji wykonuje określone obliczenia wykorzystując pierwszą przekazaną zmienną, a wynik umieszcza w drugiej. Jak widać może zdarzyć się sytuacja, w której wątek numer 0 będzie wykonywał operację sum = b + c w momencie, gdy wartość c nie będzie jeszcze określona (wątek 1 nie zakończy działania). Aby nie zaszła taka sytuacja kolizyjna należy umieścić po bloku sections dyrektywę barrier lub po prostu wykasować klauzulę nowait.

OpenMP.pl - polski portal użytkowników standardu OpenMP.