Kurs jest częścią programu Xtreme Computing.

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

Krok 3 - wyodrębnianie obszarów zrównoleglanych, opcja sections.

Jedną z podstawowych zalet standardu OpenMP jest możliwość wyodrębniania bloków, które mają być wykonywane równolegle z innymi. Do tego celu służą dyrektywy sections i section. Zanim jednak zaczniemy ich opis, powiemy jeszcze kilka słów o dyrektywie parallel. Jest to bez wątpienia najważniejsza dyrektywa w OpenMP. Informuje ona kompilator gdzie zaczyna się kod, który ma być przetworzony wielowątkowo. Dyrektywa ta tworzy grupę wątków (team) wykonujących współbieżnie określony blok instrukcji. Bez niej program jest wykonywany standardowo, czyli sekwencyjnie. Ogólna składnia dyrektywy parallel wygląda następująco:

#pragma omp parallel { // blok kodu }
Po dyrektywie parallel mogą również występować dodatkowe opcje. W poprzednich lekcjach podaliśmy już przykład jej użycia (przy okazji zrównoleglania pętli for). Przyjrzyjmy się teraz dokładniej samej dyrektywie parallel. W tym celu spójrzmy na poniższy program.

Program 03/01 - step_0301.c

#include <stdio.h> #include <stdlib.h> #include <omp.h> int main(int argc, char *argv[]) { int nr_threads, i, id; int *tab; #pragma omp parallel shared(nr_threads, tab) private(id) { #pragma omp single { nr_threads = omp_get_num_threads(); printf("Dostepnych jest %d watki.\n", nr_threads); tab = (int*)malloc(nr_threads * sizeof(int)); } id = omp_get_thread_num(); printf("Watek nr. %d mowi Ci 'czesc' i zapisuje w tablicy " "swoj numer.\n", id); tab[id] = id; if (id == 1) printf("Ponadto watek %d mowi 'Jak sie masz?'.\n", id); if (id == 2) printf("Ponadto watek %d mowi 'pozdrawiam'.\n", id); } printf("W tablicy 'tab' kazdy watek zapisal swoj numer.\n"); for (i = 0; i < nr_threads; i++) printf("tab[%d] = %d\n", i, tab[i]); free (tab); return 0; }

Za pomocą dyrektywy parallel oraz nawiasów klamrowych wskazujemy, który fragment programu ma być wykonany równolegle. Zauważmy, że nie określamy ile wątków ma być wykorzystanych. Jeśli program zostanie wykonany na zwykłym, jednordzeniowym procesorze, wówczas ten blok kodu zostanie wykonany sekwencyjnie. W pierwszej kolejności program wyznacza liczbę dostępnych wątków (zobacz Lekcja 2) oraz przydziela pamięć zmiennej tab. Następnie każdy wątek wypisuje swój numer na ekranie. Ponieważ zadeklarowaliśmy, że zmienna id jest typu prywatnego, zatem każdy wątek będzie miał swoją kopię tej zmiennej i będzie przechowywał w niej swój numer (patrz Lekcja 1). Tablica tab jest zmienną wspólną, więc wszystkie wątki będą miały do niej dostęp. W tym samym momencie każdy wątek zapisze w odpowiednim miejscu tablicy tab swój numer. Nie wystąpi konflikt między wątkami, ponieważ każdy z nich będzie zapisywał swój numer w innym miejscu tablicy. Ponadto jeśli mamy dostępne co najmniej 3 wątki, to wątki numer 1 i 2 wypiszą na ekranie dodatkowe komunikaty. Jeśli aktywne są tylko dwa wątki, wówczas tylko wątek 1 wypisze dodatkowy komunikat. Po wyjściu z sekcji parallel program w trybie sekwencyjnym wypisuje na ekranie zawartość tablicy tab.

W celu przypisania wątkowi 2 określonego zadania odwoływaliśmy się do funkcji omp_get_thread_num. Istnieje prostszy sposób podziału zadań w sekcji parallel. Korzysta on z dyrektyw sections oraz section. Pierwsza dyrektywa, tj. sections sygnalizuje, że rozpoczynamy podział kodu na oddzielne bloki, które będą wykonywane równolegle. Druga dyrektywa - section - zaznacza dany fragment kodu, który będzie przydzielony jednemu z dostępnych wątków. Następujący program ilustruje ich użycie.

Program 03/02 - step_0302.c

#include <stdio.h> #include <stdlib.h> #include <time.h> #include <omp.h> #define ACCURACY 200000000 // Metoda Wallis'a obliczania wartosci liczby Pi. void count_pi1(double *pi) { double tmp = 1.0, a_n; long int i, N; N = ACCURACY; for (i = 1; i <= N; i++) { a_n = (double)(4.0*i*i / (4.0*i*i - 1.0)); tmp = tmp * a_n; } *pi = (double)(2.0 * tmp); } // Metoda Leibniza obliczania wartosci liczby Pi. void count_pi2(double *pi) { double tmp = 0.0, a_n; long int i, N; N = ACCURACY; for (i = 0; i < N; i++) { if (i % 2 == 0) { a_n = (double)(1.0 / (2.0*i + 1.0)); } else { a_n = (double)(-1.0 / (2.0*i + 1.0)); } tmp = tmp + a_n; } *pi = (double)(4.0 * tmp); } // Program glowny. int main(int argc, char *argv[]) { double tmp1, tmp2; double p1, p2; time_t begin_t, end_t; printf("Bez OpenMP:\n"); begin_t = time(NULL); count_pi1(&tmp1); count_pi2(&tmp2); end_t = time( NULL); printf("Metoda Wallis'a Pi = %f.\n" , tmp1); printf("Metoda Leibniz'a Pi = %f.\n" , tmp2); printf("Czas wykonywania obliczen: %f.\n\n", difftime(end_t, begin_t)); printf("Z OpenMP:"); begin_t = time(NULL); #pragma omp parallel sections private(p1, p2) { #pragma omp section { count_pi1(&p1); printf("Metoda Wallis'a Pi = %f - obliczenia wykonane przez " "watek nr. %d.\n", p1, omp_get_thread_num()); } #pragma omp section { count_pi2(&p2); printf("Metoda Leibniz'a Pi = %f - obliczenia wykonane przez " "watek nr. %d.\n", p2, omp_get_thread_num()); } } end_t = time(NULL); printf("Czas wykonywania obliczen: %f.\n\n", difftime(end_t, begin_t)); return 0; }

Program oblicza dwiema metodami przybliżoną wartość liczby Pi. Dokładność tych obliczeń regulowana jest parametrem ACCURACY. Aplikacja wykonuje najpierw obliczenia w trybie sekwencyjnym. Czas tych obliczeń wyświetlany jest na ekranie. W drugiej części programu, za pomocą dyrektyw sections i section zlecamy dwóm różnym wątkom wykonanie tych samych obliczeń co w części sekwencyjnej programu. Jeden wątek będzie przybliżał wartość liczby Pi metodą Wallis'a, natomiast drugi metodą Leibniz'a. Obliczenia te będą wykonywane równolegle, przez co czas obliczeń ulegnie znacznemu skróceniu.

Zarówno w przypadku dyrektywy parallel jak i sections sposób użycia i działanie opcji shared oraz private są takie same jak przy zrównoleglaniu pętli for (zobacz Lekcja 1 i Lekcja 2).

Używając dyrektyw section i sections należy pamiętać o kilku szczegółach. Między innymi, fragmenty programu zawarte w różnych sekcjach muszą być od siebie niezależne. Tzn. obliczenia wykonywane przez jeden wątek nie mogą jednocześnie odwoływać się do obliczeń wykonywanych przez wątek drugi. Mogłoby to prowadzić do błędnego działania programu. Gdyby np. pierwszy wątek potrzebował wyników obliczeń drugiego wątku, które jeszcze się nie zakończyły. Kolejna rzecz, na którą chcemy zwrócić uwagę to fakt, że każdy blok programu ograniczony nawiasami klamrowymi opcji section jest wykonywany przez przypisany mu wątek dokładnie raz. Jeśli mamy do dyspozycji co najmniej dwa wątki wówczas funkcje count_pi1 oraz count_pi2 będą wykonywane równolegle. W przypadku, gdy mamy do dyspozycji tylko jeden wątek obliczenia zawarte po dyrektywie sections zostaną przeprowadzone sekwencyjnie. Nie mamy jednak wpływu na to w jakiej kolejności zostaną wywołane funkcje count_pi1 oraz count_pi2. Może się zdarzyć sytuacja, że funkcja count_pi2 zostanie wywołana przed funkcją count_pi1.

Ćwiczenia:

  1. Nie wykorzystując opcji sections spróbuj napisać program, który robi to samo co Program 03/02 - metodę podziału zadań między wątki weź z programu Program 03/01.

Kolejne lekcje tutorialu:

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