Problemy współbieżności
wyścig (race condition) synchronizacja realizowana sprzętowo (np. komputery macierzowe) realizowana programowo (bariera, sekcja krytyczna, operacje atomowe) wzajemne wykluczanie sekcja krytyczna protokoły wejścia i wyjścia z sekcji krytycznej cechy rozwiązań zagadnień synchronizacji: pożądane bezpieczeństwo i żywotność, uczciwość możliwe błędy - zakleszczenie, zagłodzenie
Zamki (locks, sygnalizatory dostępu, blokady) int zamek=0; procedura_wątek(){ while (zamek!= 0) { // aktywne czekanie (busy wait) zamek = 1; sekcja_krytyczna(); zamek = 0; Problemy: aktywne czekanie zużywa zasoby komputera procedura nie jest bezpieczna ani nie zapewnia żywotności
Semafory semafor jest zmienną globalną, na której można dokonywać dwóch niepodzielnych, wykluczających się operacji, zwyczajowo nazywanych: P (probeer, wait) i V (verhoog, signal) P i V często są zaimplementowane w jądrze systemu operacyjnego, gdyż zawierają operacje atomowe P( int s ){ if( s>0 ) s--; else uśpij_wątek(); V( int s ) { if( ktoś_śpi() ) obudź_wątek(); else s++; inicjacja semafora, np. init( int s, int v ) { s=v; semafory umożliwiają poprawne rozwiązanie problemu wzajemnego wykluczania implementacja V decyduje o uczciwości semafora (np. FIFO) wartość s oznacza liczbę dostępów do zasobu np. semafor binarny
Problem producentów i konsumentów Problem czytelników i pisarzy Problem ucztujących filozofów
Procesy współdzielą bufor dla produkowanych jednostek. Producent tworzy produkt i umieszcza w buforze. Konsument pobiera produkt z bufora Problem: Jak nie doprowadzić do przepełnienia bufora oraz zapobiec zagłodzeniu konsumenta?
Rozwiązanie: int sem_pełny = 0; int sem_pusty = ROZMIAR_BUFORA; void producent() { while (true) { produkuj(produkt); P(sem_pusty); //zmniejsza dodajproduktdobufora(produkt); V(sem_pełny); //zwiększa
Rozwiązanie: void konsument() { while (true) { P(sem_pełny); //zmniejsza pobierzproduktzbufora(produkt); V(pusty); //powiększa konsumuj (produkt)
Procesy czytające i piszące Czytelnicy jednoczesny dostęp do zasobu Pisarze wyłączny dostęp do zasobu brak dostępu dla innego pisarza oraz dla czytelników Problem jak nie zagłodzić czytelników ani pisarzy? Rozwiązanie: Scheduling kolejka FIFO
Dla 5ciu filozofów: filozof: albo je, albo myśli filozofowie siedzą przy stole, każdy ma talerz, pomiędzy każdymi dwoma talerzami leży widelec na środku stołu stoi misa z spaghetti problem polega na tym, że do jedzenia spaghetti potrzebne są dwa widelce (po obu stronach talerza) jak zapewnić przeżycie filozofom? Źródło: Benjamin D. Esham / Wikimedia Commons, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=56559
rozwiązanie proste dopuszczające blokadę (niepoprawne) widelec[i], i=0..4 //pięć semaforów binarnych dla pięciu //widelców (zainicjowanych wartością 1) wątek_filozof (int i) { //procedura dla i-tego filozofa (pięć współbieżnie //realizowanych wątków, i=0..4) for(;;) { myśl(); wait(widelec[i]); wait(widelec[(i+1) mod 5]); jedz(); signal(widelec[i]); signal(widelec[(i+1) mod 5]);
rozwiązanie poprawne, nieco bardziej skomplikowane widelec[i], i=0..4 //pięć semaforów binarnych dla pięciu //widelców (zainicjowanych wartością 1) pozwolenie //semafor poczwórny (=4) wątek_filozof (int i) { //procedura dla i-tego filozofa (pięć współbieżnie //realizowanych wątków, i=0..4) for(;;) { myśl(); wait(pozwolenie); wait(widelec[i]); wait(widelec[(i+1) mod 5]); jedz(); signal(widelec[i]); signal(widelec[(i+1) mod 5]); signal(pozwolenie);
Specyfikacja POSIX: muteks mutual exclusion tworzenie muteksa int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutex attr_t *mutexattr) zamykanie muteksa (istnieje wersja pthread_mutex_trylock) int pthread_mutex_lock (pthread_mutex_t *mutex) otwieranie muteksa int pthread_mutex_unlock (pthread_mutex_t *mutex) odpowiedzialność za poprawne użycie muteksów (gwarantujące bezpieczeństwo i żywotność) spoczywa na programiście
#include<pthread.h> #define LICZBA 100 #define LICZBA_W 4 pthread_mutex_t muteks; int suma=0; pthread_t watki[liczba_w]; int main( int argc, char *argv[] ) { int i, indeksy[liczba_w]; for(i=0;i<liczba_w;i++) indeksy[i]=i; pthread_mutex_init( &muteks, NULL); for(i=0; i<liczba_w; i++ ) pthread_create( &watki[i], NULL, suma_w, (void *) &indeksy[i] ); for(i=0; i<liczba_w; i++ ) pthread_join( watki[i], NULL ); printf( suma = %d\n,suma);
void *suma_w (void *arg_wsk) { int i, moj_id, moja_suma=0; moj_id = *( (int *) arg_wsk ); j=liczba/liczba_w; for( i=j*moj_id+1; i<=j*(moj_id+1); i++) { moja_suma += i; pthread_mutex_lock( &muteks ); suma += moja_suma; pthread_mutex_unlock( &muteks ); pthread_exit( (void *)0 );
Zmienne warunku condition variables: zmienne warunku są zmiennymi wspólnymi dla wątków, które służą do identyfikacji grupy uśpionych wątków tworzenie zmiennej warunku: int pthread_cond_init( pthread_cond_t *cond, pthread_condattr_t *cond_attr ) uśpienie wątku w miejscu identyfikowanym przez zmienną warunku cond, wątek śpi (czeka) do momentu, gdy jakiś inny wątek wyśle odpowiedni sygnał budzenia dla zmiennej cond int pthread_cond_wait( pthread_cond_t *cond, pthread_mutex_t *mutex ) sygnalizowanie (budzenie pierwszego oczekującego na zmiennej *cond) int pthread_cond_signal( pthread_cond_t *cond ) rozgłaszanie sygnału (budzenie wszystkich oczekujących na zmiennej *cond) int pthread_cond_broadcast( pthread_cond_t *cond)
Schemat rozwiązania problemu producenta i konsumenta pthread_mutex_t muteks=pthread_mutex_initializer; pthread_cond_t nie_pelna, nie_pusta; // należy także // zainicjować int main() { pthread_t producent, konsument; kolejka *fifo; fifo = inicjuj_kolejka(); // kolejka zawiera bufor do zapisu i odczytu pthread_create( &producent, NULL, produkuj, fifo ); pthread_create( &konsument, NULL, konsumuj, fifo ); pthread_join( producent, NULL); pthread_join( konsument, NULL );
procedura producenta void *produkuj( void *q) { kolejka *fifo; int i; fifo = (kolejka *)q; for(...) { pthread_mutex_lock ( &muteks); while( kolejka_pelna(fifo) ) pthread_cond_wait( &nie_pelna, &muteks ); kolejka_wstaw(fifo,...); pthread_mutex_unlock( &muteks ); pthread_cond_signal( &nie_pusta );
procedura konsumenta void *konsumuj( void *q) { kolejka *fifo; int i, d; fifo = (kolejka *)q; for(...) { pthread_mutex_lock (&muteks); while( kolejka_pusta(fifo) ) pthread_cond_wait(&nie_pusta, &muteks); kolejka_pobierz(fifo,...); pthread_mutex_unlock( &muteks ); pthread_cond_signal( &nie_pelna );
Zależności wzajemne uzależnienie instrukcji nakładające ograniczenia na kolejność ich realizacji Zależności zasobów kiedy wiele wątków jednocześnie usiłuje korzystać z wybranego zasobu (np. pliku)
Zależności sterowania kiedy wykonanie danej instrukcji zależy od rezultatów poprzedzających instrukcji warunkowych Zależności danych (zależności przepływu) kiedy instrukcje wykonywane w bezpośrednim sąsiedztwie czasowym operują na tych samych danych i choć jedna z tych instrukcji dokonuje zapisu
1 if (a==b) 2 a = a + b; 3 b = a + b; Linijka 2 kontroluje warunek w linijce 1, linijka 3 wykona się zawsze bez względu na wynik warunku 1.
Struktura danych używana przez kompilator do reprezentacji procedur w programie. Wierzchołkami są bloki podstawowe a krawędzie pokazują powiązania między blokami.
Front dominacji
zależności wyjścia (zapis po zapisie, write-afterwrite) a :=... a :=... Zależności wyjścia nie są rzeczywistym problemem; odpowiednia analiza kodu może doprowadzić do wniosku o możliwej eliminacji instrukcji lub przemianowaniu zmiennych
Problem: x=4; y=x+2; x=5; Rozwiązanie: x2=4; y=x2+2; x=5;
anty-zależności (zapis po odczycie, write-afterread)... := a a :=... także tutaj odpowiednie przemianowanie zmiennych może zlikwidować problem
Problem: x=4; y=x+2; x=5; Rozwiązanie: x=4; x2 = x; y=x2+2; x=5;
zależności rzeczywiste (odczyt po zapisie, readafter-write) a :=...... := a zależności rzeczywiste uniemożliwiają zrównoleglenie algorytmu konieczne jest jego przeformułowanie w celu uzyskania wersji możliwej do zrównoleglenia
x=5; y=x; z=y; Każda instrukcja zależy od poprzedniej!
Wszystkie powyższe zależności mogą występować albo jawnie jak w podanych przykładach, lub niejawnie jako tzw. zależności przenoszone przez pętle
Zależności w kodzie: 1. x = 2*y + w; 2. y = 2*z cos(w); 3. z = log(w) + y; 4. x += sin(z); 5. w = 5*y; x: 1-4 -> WAW y: 1-2 -> WAR, 2-3 -> RAW, 2-5 -> RAW z: 2-3 -> WAR, 3-4 -> RAW w: 1-5 -> WAR, 2-5 ->WAR, 3-5 -> WAR
Możliwe zrównoleglenie usunięcie zależności narzut wykonania równoległego Przykłady zależności przenoszonych w pętli zrównoleglenie narzut wykonania równoległego poprawność zależna od operacji w skrajnych iteracjach
zależności wyjścia (output dependencies) ewentualne zrównoleglenie działania po przemianowaniu zmiennych musi zachować kolejność zapisu danych anty-zależności (anti-dependencies) możliwe zrównoleglenie działania poprzez przemianowanie zmiennych zależności rzeczywiste (true dependencies) W przypadku użycia wskaźników automatycznie zrównoleglające kompilatory mogą podejrzewać występowanie utożsamienia (zamienności) nazw (aliasing)
zależności wyjścia for(j = 0; j < n; j++) { c[j] = j; c[j+1] = 5; anty-zależności for(j = 0; j < n; j++) b[j] = b[j+1];
zależności rzeczywiste (true dependencies) for(j = 0; j < n; j++) a[j] = a[j-1]; Zależności przenoszone przez pętle for(i = 0; i < 4; i++) { b[i] = 8; a[i] = b[i-1] + 10; for(i = 0; i < 4; i++) { b[i] = 8; a[i] = b[i] + 10;
Zależności przenoszone przez pętle for(i = 0; i < 4; i++) for(j = 0; j < 4; j++) a[i][j] = a[i][j-1] * x;
W celu przeprowadzenia analizy zależności w programie można konstruować i badać grafy zależności Dla praktycznie występujących programów konstrukcja i analiza grafu zależności przekracza możliwości komputerów (problem jest NP-trudny) Kompilatory zrównoleglające stosują rozmaite metody heurystyczne analizy kodu W wielu wypadkach analiza nie powodzi się (zwłaszcza, gdy np. poszczególne wątki wywołują procedury) Ingerencja projektanta algorytmu i programisty okazuje się być często jedynym sposobem na uzyskanie efektywnego programu równoległego
Dziękuję za uwagę