Modele programowania równoległego Pamięć współdzielona Rafał Walkowiak dla III roku Informatyki PP
Procesy a wątki [1] Model 1: oparty o procesy - Jednostka tworzona przez system operacyjny realizująca program posiadająca powiązany z nią zbiór zasobów: struktury danych z informacjami o procesie, wirtualna przestrzeń adresowa zawierająca program i dane oraz wątek przetwarzania. Tworzenie i przełączanie procesów realizowane przez jądro systemu operacyjnego. Tworzenie kopii danych procesu. Przestrzenie adresowe procesów niezaleŝne, ne, pamięć współdzielona definiowana oddzielnie dostępna za pomocą dedykowanych funkcji. Silne zabezpieczenie praw dostępu do danych, narzuty czasowe związane z zabezpieczeniami praw (typowe dla aplikacji róŝnych uŝytkowników niepotrzebne dla przetwarzania równoległego i współpracy). Model fork/exec tworzenie i łączenie ścieŝek przetwarzania. Wywołania shmget/mmap do współdzielenia pamięci. Standardowe funkcje dla większości UNIXów. 12/4/2011 Pamięć współdzielona 2
Procesy a wątki [2] Model 2 - oparty o wątki: Wątek przetwarzania to niezaleŝna ścieŝka realizacji programu powiązana ze stanem rejestrów procesora. Wątki programu współdzielą pamięć, pliki i inne atrybuty procesu - dane i kod nie są powielone. KaŜdy wątek ma własny stos i zmienne lokalne na stosie. NiŜszy poziom zabezpieczeń dostępu do zasobów. Model efektywny ze względu na niŝsze narzuty czasowe obsługi. Wątki mogą być zawieszone zrzekają się procesora lub aktywnie sprawdzać stan zmiennej określającej ich stan oczekiwania. Efekt -brak moŝliwości wykorzystania procesora przez inne wątki, lecz mniejsze koszty wznowienia przetwarzania niŝ dla wątku zawieszonego. Efektywny jest powrót wątku na ten sam procesor ze względu na dane w pamięci podręcznej juŝ pobrane wcześniej (niektóre systemy zapewniają). 12/4/2011 Pamięć współdzielona 3
Cechy modelu pamięci współdzielonej z wątkami przetwarzania Przenośność oprogramowania maszyny szeregowe i równoległe. Ukrywanie opóźnień dostępu do pamięci, I/O, komunikacja dzięki wielu wątkom na jednym procesorze jeden czeka drugi jest przetwarzany. Zapewnia wspomaganie programisty na poziomie szeregowania i równowaŝenia obciąŝeń. Łatwiejsze programowanie niŝ przez przekazywanie komunikatów, lecz dopiero dodatkowy wysiłek optymalizacyjny pozwala osiągnąć analogiczną efektywność. Standard API z wątkami to POSIX API Pthreads. 12/4/2011 Pamięć współdzielona 4
Podstawy pracy z wątkami Tworzenie i kończenie wątku pthread_create parametry: identyfikator watku uchwyt, kod funkcji, argumenty funkcji pthread_exit zakończenie funkcji kodu wątku pthread_ join parametr: identyfikator wątku - wstrzymanie przetwarzania do momentu zakończenia wskazywanego wątku. Wzajemne wykluczenie (sekcja krytyczna kodu) zamek pthread_mutex_lock, pthread_mutex_unlock, pthread_mutex_trylock (wersja nieblokująca, jest szybsza, wątek moŝe realizować własne prace) WaŜne jest zapewnienie w programie: moŝliwe krótkich sekcji krytycznych, moŝliwie rzadki dostęp do zmiennych współdzielonych (zapis do zmiennych lokalnych i uaktualnienie zbiorcze rzadsze: synchronizacja i konieczność uaktualniania pamięci podręcznej po zapisie). 5
Synchronizacja ze zmienną warunkową pthread_cond_wait zamek warunkowy, parametry: zmienna warunkowa i zamek; Wątek zakłada standardowy zamek, a następnie jest zawieszany w oczekiwaniu na sygnalizację dla zmiennej warunkowej przez inny wątek, zamek jest zdejmowany przy zawieszeniu wątku, sygnalizacja zmiennej warunkowej powoduje wznowienie zablokowanego wątku, który oczekuje na pozyskanie zamka przed wyjściem z funkcji. Taki mechanizm umoŝliwia zrealizowanie w sposób wykluczający pracy po sygnalizacji dla zmiennej warunkowej związanej z tym zamkiem. Wznowienie zawieszonego wątku (wątków) w wyniku sygnalizacji pthread_cond_signal (lub pthread_cond_broadcast). Wykorzystanie przerwania, a nie cyklicznego sprawdzania wyŝsza efektywność rozwiązania niŝ sam zamek - oddawanie procesora przez wątek. Dostępna równieŝ wersja z określonym czasem oczekiwania na sygnalizację zmiennej warunkowej. Dostępne równieŝ wersje zamków: wielokrotnego i pojedynczego z testowaniem poprawności (błąd wielokrotnego zamykania). 12/4/2011 Pamięć współdzielona 6
Przykładowe złoŝone mechanizmy synchronizacyjne Bariera słuŝy do wstrzymywania przetwarzania wcześniejszych wątków do momentu osiągnięcia przez wszystkie wątki z załoŝonego zbioru określonego miejsca w kodzie. licznik i zamek - nieefektywna ze względu na cykliczne sprawdzanie stanu licznika, licznik i warunkowy zamek prosty rozwiązanie efektywne, porównaj slide 10. Zamek odczytu i zapisu zamek złoŝony zbudowany w oparciu o warunkowy zamek prosty. UmoŜliwia obsługę wielu czytelników i pisarzy zapewniając spójną zawartość modyfikowanej informacji. Odczyt bez potrzeby zapewnienia spójności, zapis wymaga zapewnienia spójności. 12/4/2011 Pamięć współdzielona 7
Zamek odczytu i zapisu- przykład problem wielu pisarzy i czytelników Zamek odczytu i zapisu bazuje na strukturze zawierającej informacje: licznik czytających czytelników, znacznik pisarz pisze, zmienna warunkowa zezwolenia na pisanie, zmienna warunkowa zezwolenia na odczyt, licznik oczekujących pisarzy i zamek ochrony dostępu do zmiennych. Funkcja zezwolenia na odczyt po pozyskaniu zamka czeka na sygnalizację w przypadku gdy pisarz pisze lub są pisarze oczekujący(priorytet!). Po sygnalizacji zwiększa licznik czytelników i zwalnia zamek. 12/4/2011 Pamięć współdzielona 8
Zamek odczytu i zapisu- przykład Funkcja zezwolenia na pisanie po pozyskaniu zamka w przypadku gdy pisarz pisze lub gdy są czytelnicy zwiększa licznik oczekujących pisarzy i czeka na sygnalizację przy zmiennej zezwalającej na pisanie. Po sygnalizacji zmniejsza liczbę czekających pisarzy, modyfikuje pisarz pisze i zwalnia zamek. Funkcja rezygnacji z dostępu (wspólna dla pisarzy i czytelników) po pozyskaniu zamka: jeŝeli pisarz pisał usuwa znacznik pisarz pisze a w przeciwnym razie koniec czytania zmniejsza licznik czytelników, jeŝeli nie ma czytających i są oczekujący pisarze to sygnalizuje czekającemu na pisanie a w przeciwnym razie realizowane jest rozgłaszanie dla wszystkich oczekujących na czytanie, zwalnia zamek. 12/4/2011 Pamięć współdzielona 9
1 typedef struct { 2 int readers; //liczba czytających 3 int writer; //piszący 0/1 4 pthread_cond_t readers_proceed; 5 pthread_cond_t writer_proceed; 6 int pending_writers; // czekajacy 7 pthread_mutex_t read_write_lock; 8 } rwlock_t; 9 void rwlock_init (rwlock_t *l) { 10 l -> readers = l -> writer = l -> pending_writers = 0; 11 pthread_mutex_init(&(l -> read_write_lock), NULL); 12 pthread_cond_init(&(l -> readers_proceed), NULL); 13 pthread_cond_init(&(l -> writer_proceed), NULL); 14 } 15 void rwlock_rlock(rwlock_t *l) { 16 pthread_mutex_lock(&(l -> read_write_lock)); 17 while ((l -> pending_writers > 0) (l -> writer > 0)) 18 pthread_cond_wait(&(l -> readers_proceed), 19 &(l -> read_write_lock)); 20 l -> readers ++; 21 pthread_mutex_unlock(&(l -> read_write_lock)); 22 } 23 void rwlock_wlock(rwlock_t *l) { 24 pthread_mutex_lock(&(l -> read_write_lock)); 25 while ((l -> writer > 0) (l -> readers > 0)) { 26 l -> pending_writers ++; 27 pthread_cond_wait(&(l -> writer_proceed), 28 &(l -> read_write_lock)); 29 l -> pending_writers --; 30 } 31 l -> writer ++ 32 pthread_mutex_unlock(&(l -> read_write_lock)); 33 } 34 void rwlock_unlock(rwlock_t *l) { 35 pthread_mutex_lock(&(l -> read_write_lock)); 36 if (l -> writer > 0) 37 l -> writer = 0; 38 else if (l -> readers > 0) 39 l -> readers --; 40 if ((l -> readers == 0) && (l -> pending_writers > 0)) 41 pthread_cond_signal(&(l -> writer_proceed)); 42 else 43 if (l -> pending_writers == 0) thread_cond_broadcast(&(l -> readers_proceed)); 44 pthread_mutex_unlock(&(l -> read_write_lock)); 45 } 12/4/2011 Pamięć współdzielona 10
void rwlock_rlock(rwlock_t *l) { pthread_mutex_lock(&(l -> read_write_lock)); while ((l -> pending_writers > 0) (l -> writer > 0)) pthread_cond_wait(&(l -> readers_proceed), &(l -> read_write_lock)); l -> readers ++; pthread_mutex_unlock(&(l -> read_write_lock)); } 12/4/2011 Pamięć współdzielona 11
void rwlock_wlock(rwlock_t *l) { pthread_mutex_lock(&(l -> read_write_lock)); while ((l -> writer > 0) (l -> readers > 0)) { l -> pending_writers ++; pthread_cond_wait(&(l -> writer_proceed), &(l -> read_write_lock)); l -> pending_writers --; } l -> writer ++ pthread_mutex_unlock(&(l -> read_write_lock)); } 12/4/2011 Pamięć współdzielona 12
void rwlock_unlock(rwlock_t *l) { pthread_mutex_lock(&(l -> read_write_lock)); if (l -> writer > 0) l -> writer = 0; else if (l -> readers > 0) l -> readers --; if ((l -> readers == 0) && (l -> pending_writers > 0)) pthread_cond_signal(&(l -> writer_proceed)); else if (l -> pending_writers == 0) thread_cond_broadcast(&(l -> readers_proceed)); pthread_mutex_unlock(&(l -> read_write_lock)); } 12/4/2011 Pamięć współdzielona 13
Typedef struct{ pthread_mutex_t zamek; pthread_cond_t zgoda; int licznik; }bariera_t; void init_bariera(bariera_t *b){ } b->licznik=0; Bariera synchronizacyjna pthread_mutex_init(&b->zamek),null); pthread_cond_init(&b->zgoda),null); void bariera (bariera_t *b, int num_threads){ } pthread_mutex_lock(&b->zamek); b->licznik++; if (b->licznik == num_threads){ } else b->licznik =0; pthread_cond_broadcast(&(b->zgoda)); while( pthread_cond_wait(&(b->zgoda), &(b->zamek))!=0); pthread_mutex_unlock(&(b->zamek)); Wątki po wejściu w barierę zostają zawieszone do momentu, gdy pthread_cond_broadcast pozwoli na wznowienie ich przetwarzania. Wznowienie przetwarzania jest realizowane szeregowo ze względu na konieczność pozyskiwania zamka przy wychodzeniu z funkcji warunkowego oczekiwania (moŝna zastosować zamek wielopoziomowy hierarchiczny dla przyspieszenia wznawiania pracy). Warunkowe oczekiwanie realizowane jest w pętli z warunkiem testującym czy przyczyną obudzenia wątku jest sygnalizacja od systemu operacyjnego, czy teŝ (oczekiwana) sygnalizacja związana ze określoną zmienną warunkową. 14
Zasady projektowania programów asynchronicznych (komunikacja przez pamięć współdzieloną) OstrzeŜenia: Nie zakładać z góry określonej kolejności odwołań do zmiennych. Np. proces korzystający z danych powinien być utworzony po ich zainicjowaniu. Zmienne przekazywane przez stos lub globalne mogą zostać nadpisane ze względu na usunięcie procesu, w którym były zadeklarowane. Nie wnioskować z zasad szeregowania procesów w celu przewidywania kolejności przetwarzania (wnioskowanie wraŝliwe na zmianę algorytmu szeregowania lub liczby i typów procesorów np. zmiana systemu obliczeniowego). Reguły: Wszystkie parametry wątku zainicjować przed utworzeniem wątku (później moŝe to nie być moŝliwe). Przygotować dane (producent) przed momentem ich wykorzystywania (konsument), zapewnić odpowiednią wielkość buforów (komunikacja asynchroniczna). Udostępnić dane wszystkim potencjalnym konsumentom. Stosować synchronizację grupową i powielenie danych dla wzrostu efektywności przetwarzania. Minimalizować narzuty czasowe synchronizacji i unikać programowania w sposób prowadzący do wyścigu (race condition) 12/4/2011 Pamięć współdzielona 15
OpenMP Przenośny standard programowania równoległego oparty o dyrektywy kompilatora. Dyrektywy automatycznie tłumaczone na wywołania API Pthreads. Forma dyrektyw: #pragma omp dyrektywa [lista klauzul] Przykłady dyrektyw: #pragma omp parallel num_threads(8) #pragma omp segments #pragma omp master #pragma omp single #pragma omp for schedule (e.g. static, dynamic, guided, runtime); reduction(operator: zmienna podlegająca scaleniu); private(lista zmiennych); shared(lista zmiennych); #pragma omp critical [(nazwa)] #pragama omp atomic Funkcje zamków. Zmienne środowiskowe (liczba wątków, szeregowanie itp.) 12/4/2011 Pamięć współdzielona 16