headerphoto

OpenMP

Zmienne w OpenMP

Kolejnym, bardzo ważnym elementem równoległego przetwarzania danych są zmienne. Pod tym określeniem kryją się wydzielone obszary pamięci o określonej długości i typie. Standard OpenMP umożliwia dodatkowo nadawanie zmiennym pewnych specyficznych cech i właściwości. Dla przykładu, zmienna biorąca udział w równoległym przetwarzaniu pętli for może być współdzielona przez wszystkie wątki biorące udział w zrównoleglaniu. Oznacza to, że każdy wątek może bez przeszkód dokonywać odczytu i modyfikacji wartości tej zmiennej, a inne wątki dynamicznie zauważą te zmiany. Takie zachowanie jest charakterystyczną cechą zmiennych współdzielonych (ang. shared variables). W kodzie programu, określa się je za pomocą klauzuli shared ().

Może być też tak, że każdy wątek posiada lokalne kopie zmiennych, na których dokonuje dowolnych modyfikacji. Zmiany wartości zmiennej w obrębie jednego wątku, nie mają żadnego wpływu na zmienną o tej samej nazwie w granicach innego. Są to tzw. zmienne lokalne (ang. private), opatrzone klauzulą private (). Kontynuując przykład pętli for, tego typu zmienne sprawdzają się doskonale, np. jako liczniki iteracji. Każdy wątek dostaje swoja własną kopię zmiennej z aktualną wartością licznika. A co za tym idzie, może spokojnie dokonywać obliczeń w oparciu o tę zmienną, bez obawy że inny wątek "niechcąco" zmodyfikuje ją w trakcie przetwarzania swojej porcji danych.

Poniższy fragment kodu posłuży jako ilustracja do obu typów zachowań zmiennych w czasie równoległego przetwarzania iteracji w pętli for.

suma_wspolna=0;
#pragma omp parallel private(numer_iteracji, suma_prywatna)
                     shared (n, suma_wspolna)
 {
    suma_prywatna = 0;  // zmienna prywatna
                n = 5;  // licznik iteracji
     #pragma omp for
      for (numer_iteracji=0; numer_iteracji < n; numer_iteracji++)
       {
        suma_wspolna += numer_iteracji;
        suma_prywatna += numer_iteracji;
         printf("Watek %d, iteracja = %d, suma_wspolna = %d,
         suma_prywatna = %d", omp_get_thread_num(),
         numer_iteracji, suma_wspolna, suma_prywatna);
       }
  }

Program zostanie uruchomiony na dwóch różnych wątkach, a pętla for "obróci się" pięć razy. W czasie działania zostaną zmodyfikowane tylko dwie zmienne: jedna współdzielona zmienna typu int (suma_wspolna) i druga prywatna (suma_prywatna). Obie przed rozpoczęciem obliczeń zostały wyzerowane. Celem programu jest proste sumowanie numerów aktualnie wykonywanych iteracji. Zsumowane liczby przechowujemy w zmiennej współdzielonej oraz prywatnej. Tak jak wcześniej wspomniano, pętla uruchomi się pięć razy, a więc prawidłowym wynikiem sumowania powinna być wartość: 0+1+2+3+4 = 10. Po uruchomieniu programu otrzymujemy następujące wyniki:

Watek 1, iteracja = 3, suma_wspolna = 3,   suma_prywatna = 3
Watek 1, iteracja = 4, suma_wspolna = 7,   suma_prywatna = 7
Watek 0, iteracja = 0, suma_wspolna = 7,   suma_prywatna = 0
Watek 0, iteracja = 1, suma_wspolna = 8,   suma_prywatna = 1
Watek 0, iteracja = 2, suma_wspolna = 10,  suma_prywatna = 3

Jak widać, zmienna współdzielona (suma_wspolna) poprawnie dodała wszystkie numery iteracji. Było to możliwe, ponieważ zmienne współdzielone widoczne są dla wszystkich wątków i każdy z nich może dokonywać modyfikacji na tego typu zmiennych. Nie inaczej było w przypadku zmiennej suma_wspolna. Jej wartość nie znikała wraz z wątkiem, który zakończył przetwarzanie swojej porcji danych, lecz była przekazana kolejnym, które mogły ją zaktualizować. Nieco inaczej przedstawia się sytuacja zmiennych prywatnych. W naszym przykładzie reprezentantem tego rodzaju zmiennych była suma_prywatna. Analizując wyniki programu, nie trudno zauważyć pewną zależność. Otóż zmienna suma_prywatna była poprawnie aktualizowana dopóki, dopóty istniał wątek który nią zarządzał. Kiedy wątek kończył swoje działanie, ginęły także wartości zapisane w zmiennej suma_prywatna. Jest to charakterystyczna cecha zmiennych prywatnych. Każdy wątek tworzy lokalną kopię prywatnej zmiennej, na potrzeby swoich własnych obliczeń. W przypadku sumowania otrzymaliśmy tak naprawdę dwa cząstowe wyniki. Wątek 0 w ramach swojej pracy wyznaczył sumę numerów iteracji równą 3, zaś wątek 1 rozpoczął równocześnie swoje własne obliczenia z których wynikło, że suma równa jest 7.

Podsumowując, zmienne lokalne doskonale nadają się do przechowywania danych w ramach jednego wątku. Wartości tych zmiennych nie są widoczne poza obszarem działania każdego z nich. Z kolei zmienne współdzielone to obszary pamięci, które mogą być dowolnie modyfikowane przez różne wątki, a wartości tych zmiennych przechowywane są do samego końca obszaru zrównoleglania.

Jak wynika z tej definicji, wartości zapisane w zmiennych prywatnych przechowywane są do chwili zniszczenia wątku, który nimi zarządza. Czasami zdarza się jednak, że zależy nam na odczytaniu ostatnich stanów, jakie przyjmowały niektóre zmienne prywatne. Teoretycznie wartość ta usuwana jest wraz z wątkiem kończącym przetwarzanie obszaru zrównoleglania. Można jednak użyć pewnej sztuczki, dzięki której ostatnia wartość zmiennej lokalnej zostanie skopiowana do pierwotnej zmiennej o tej samej nazwie (widocznej poza obszarem zrównoleglania). Zabiegi tego typu stosuje się przy zrównoleglaniu pętli for. Aby nadać zmiennej prywatnej te specyficzne właściwości, należy zaklasyfikować ją do klauzuli lastprivate() w kodzie zrównoleglającym pętlę for. Różnica w interpretacji zmiennych prywatnych i lastprivate przedstawiona jest na poniższym przykładzie. Mamy tutaj kod prostej pętli for służącej do inkrementacji zmiennej liczba. Dla uproszczenia załóżmy, że w zrównoleglaniu bierze udział tylko jeden wątek.

...
int liczba = 1;
int n = 5; // liczba obrotów pętli
...
#pragma omp for private (liczba)
for (i=0; i < n; i++)
{
liczba += 1;
}
printf("liczba = %d", liczba);
...
int liczba = 1;
int n = 5; // liczba obrotów pętli
...
#pragma omp for lastprivate (liczba)
for (i=0; i < n; i++)
{
liczba += 1;
}
printf("liczba = %d", liczba);
WYNIK WYNIK
liczba = 1 liczba = 5

W pierwszym przypadku wyniki były łatwe do przewidzenia. Dzięki zaklasyfikowaniu zmiennej liczba jako zmiennej prywatnej, po zakończeniu zrównoleglania jej wartość nie uległa zmianie. Z kolei przy zastosowaniu klauzuli lastprivate, nowa wartość zmiennej liczba, została skopiowana (zaraz po zakończeniu przetwarzania równoległego) do zmiennej pierwotnej, widocznej poza tym obszarem zrównoleglania. W taki sposób możemy zachować wyniki pracy ostatniego wątku biorącego udział w przetwarzaniu pętli for.

Inną modyfikacją norm rządzących zmiennymi prywatnymi jest klauzula firstprivate(). Ona także odnosi się do procesu zrównoleglania pętli for. W jej przypadku nie skupiamy się już na końcowym momencie równoległego przetwarzania danych, lecz na jego początku. Za pomocą klauzuli firstprivate, możliwe jest zainicjowanie zmiennych lokalnych pewnymi wartościami, jeszcze zanim zmienne te zostaną użyte przez różne wątki. Innymi słowy, jeśli wątek zauważy, że zmienna została przekazana do obszaru zrównoleglania jako firstprivate, zainicjuje jej wartością, "swoją" zmienną lokalną o tej samej nazwie.