Dariusz Wawrzyniak Definicja semafora Klasyfikacja semaforów Implementacja semaforów Zamki Zmienne warunkowe Klasyczne problemy synchronizacji Plan wykładu (2) Semafory Rodzaje semaforów (1) Semafor jest zmienną całkowitą nieujemną lub w przypadku semaforów binarnych zmienną typu logicznego. Na semaforze można wykonywać dwa rodzaje operacji: P opuszczanie semafora (hol. proberen) V podnoszenie semafora (hol. verhogen) Synchronizacja polega na blokowaniu procesu w operacji opuszczania semafora, jeśli semafor jest już opuszczony. Semafor binarny zmienna semaforowa przyjmuje tylko wartości true (stan podniesienia, otwarcia) lub false (stan opuszczenia, zamknięcia). Semafor ogólny (zliczający) zmienna semaforowa przyjmuje wartości całkowite nieujemne, a jej bieżąca wartość jest zmniejszana lub zwiększana o 1 w wyniku wykonania odpowiednio operacji opuszczenia lub podniesienia semafora. (3) (4) Rodzaje semaforów (2) Implementacja semafora ogólnego na poziomie maszynowym Semafor uogólniony semafor zliczający, w przypadku którego zmienną semaforową można zwiększać lub zmniejszać o dowolną wartość, podaną jako argument operacji. Semafor dwustronnie ograniczony semafor ogólny, w przypadku którego zmienna semaforowa, oprócz dolnego ograniczenia wartością 0, ma górne ograniczenie, podane przy definicji semafora. Opuszczanie semafora procedure P(var s: Semaphore) while s = 0 do nic; s := s - 1; Podnoszenie semafora procedure V(var s: Semaphore) s := s + 1; instrukcje muszą być wykonane atomowo instrukcja musi być wykonana atomowo (5) (6) 1
Implementacja semafora binarnego na poziomie maszynowym Implementacja semafora ogólnego na poziomie systemu operacyjnego (1) Opuszczanie semafora procedure P(var s: Binary_Semaphore) while not s do nic; s := false; Podnoszenie semafora procedure V(var s: Binary_Semaphore) s := true; instrukcje muszą być wykonane atomowo instrukcja musi być wykonana atomowo Struktury danych type Semaphore = record wartość: Integer; L: list of Proces; Opuszczanie semafora procedure P(var s: Semaphore) s.wartość := s.wartość - 1; if s.wartość < 0 then dołącz dany proces do s.l zmień stan danego procesu na oczekujący (7) (8) Implementacja semafora ogólnego na poziomie systemu operacyjnego (2) Wzajemne wykluczanie z użyciem semaforów Podnoszenie semafora procedure V(var s: Semaphore) s.wartość := s.wartość + 1; if s.wartość 0 then wybierz i usuń jakiś/kolejny proces z kolejki s.l zmień stan wybranego procesu na gotowy shared mutex: Semaphore := 1; P(mutex); sekcja wejściowa sekcja krytyczna; V(mutex); sekcja wyjściowa reszta; (9) (10) Mechanizmy synchronizacji POSIX zmienne synchronizujące Operacje na zmiennych synchronizujących Rodzaje zmiennych synchronizujących: zamek umożliwiająca implementację wzajemnego wykluczania, zmienna warunkowa umożliwia usypianie i budzenie wątków. Zmienne synchronizujące muszą być współdzielone przez synchronizowane wątki. Zanim zmienna zostanie wykorzystana do synchronizacji musi zostać zainicjalizowana. Zamek umożliwia implementację wzajemnego wykluczania. Operacje: lock zajęcie (zaryglowanie) zamka unlock zwolnienie (odryglowanie) zamka trylock nieblokująca próba zajęcia zamka Zmienna warunkowa umożliwia usypianie i budzenie wątków. Operacje: wait uśpienie wątku, signal obudzenie jednego z uśpionych wątków broadcast obudzenie wszystkich uśpionych wątków (11) (12) 2
Zamek interfejs Zamek implementacja Typ: pthread_mutex_t Operacje: pthread_mutex_lock(pthread_mutex_t *m) zajęcie zamka pthread_mutex_unlock(pthread_mutex_t *m) zwolnienie zamka pthread_mutex_trylock(pthread_mutex_t *m) próba zajęcia zamka w sposób nie blokujący wątku w przypadku niepowodzenia pthread_mutex_lock zajęcie zamka, jeśli jest wolny ustawienie stanu wątku na oczekujący i umieszczenie w kolejce, jeśli zamek jest zajęty pthread_mutex_unlock ustawienie zamka na wolny, jeśli kolejka oczekujących wątków jest pusta wybranie wątku z niepustej kolejki wątków oczekujących i ustawienie jego stanu na gotowy. pthread_mutex_trylock zajęcie zamka lub kontynuacja przetwarzania (13) (14) Zmienna warunkowa interfejs Zmienna warunkowa implementacja Typ: pthread_cond_t Operacje: pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m) uśpienie wątku na zmiennej warunkowej, pthread_cond_signal(pthread_cond_t *c) obudzienie jednego z wątków uśpionych na zmiennej warunkowej, pthread_cond_broadcast(pthread_cond_t *c) obudzienie wszystkich wątków uśpionych na zmiennej warunkowej. pthread_cond_wait ustawienie stanu wątku na oczekujący i umieszczenie go w kolejce pthread_cond_signal wybranie jednego wątku z kolejki i postępowanie takie, jak przy zajęciu zamka zignorowanie sygnału, jeśli kolejka jest pusta pthread_cond_broadcast ustawienie wszystkich wątków oczekujących na zmiennej warunkowej w kolejce do zajęcia zamka, a jeśli zamek jest wolny zmiana stanu jednego z nich na gotowy. (15) (16) Zasada funkcjonowania zmiennej warunkowej Użycie zmiennych warunkowych (schemat 1) wątek oczekujący zignorowanie sygnału pthread_cond_wait wątek 1 wątek 2 pthread_cond_signal pthread_cond_signal pthread_mutex_lock(&m) TAK warunek spełniony? NIE sygnał pthread_cond_wait(&c,&m) pthread_mutex_unlock(&m) (17) (18) 3
Użycie zmiennych warunkowych (schemat 1) wątek sygnalizujący Użycie zmiennych warunkowych (schemat 2) wątek sygnalizujący pthread_mutex_lock(&m) modyfikacja zmiennych współdzielonych pthread_cond_signal(&c) pthread_cond_signal(&c) pthread_mutex_lock(&m) modyfikacja zmiennych współdzielonych TAK warunek spełniony? NIE sygnał pthread_mutex_unlock(&m) sygnał pthread_mutex_unlock(&m) (19) (20) Użycie zmiennych warunkowych (schemat 2) wątek oczekujący Klasyczne problemy synchronizacji pthread_mutex_lock(&m) TAK warunek spełniony? NIE pthread_mutex_unlock(&m) pthread_cond_wait(&c,&m) sygnał Problem producenta i konsumenta problem ograniczonego buforowania w komunikacji międzyprocesowej Problem czytelników i pisarzy problem synchronizacji dostępu do zasobu w trybie współdzielonym i wyłącznym Problem pięciu filozofów problem jednoczesnego dostępu do dwóch zasobów (ryzyko głodzenia i zakleszczenia) Problem śpiących fryzjerów problem synchronizacji w interakcji klient-serwer przy ograniczonym kolejkowaniu (21) (22) Problem producenta i konsumenta semaforów ogólnych (1) Producent produkuje jednostki określonego produktu i umieszcza je w buforze o ograniczonym rozmiarze. Konsument pobiera jednostki produktu z bufora i konsumuje je. Z punktu widzenia producenta problem synchronizacji polega na tym, że nie może on umieścić kolejnej jednostki, jeśli bufor jest pełny. Z punktu widzenia konsumenta problem synchronizacji polega na tym, że nie powinien on mięć dostępu do bufora, jeśli nie ma tam żadnego elementu do pobrania. const n: Integer := rozmiar bufora; shared buf: array [0..n-1] of ElemT; shared wolne: Semaphore := n; shared zajęte: Semaphore := 0; (23) (24) 4
semaforów ogólnych (2) semaforów ogólnych (3) Producent local i: Integer := 0; local elem: ElemT; while... do produkuj(elem); P(wolne); buf[i] := elem; i := (i+1) mod n; V(zajęte); Konsument local i: Integer := 0; local elem: ElemT; while... do P(zajęte); elem := buf[i]; i := (i+1) mod n; V(wolne); konsumuj(elem); (25) (26) Problem czytelników i pisarzy Synchronizacja czytelników i pisarzy za pomocą semaforów binarnych (1) Dwa rodzaje użytkowników czytelnicy i pisarze korzystają ze wspólnego zasobu czytelni. Czytelnicy korzystają z czytelni w trybie współdzielonym, tzn. w czytelni może przebywać w tym samym czasie wielu czytelników. Pisarze korzystają z czytelni w trybie wyłącznym, tzn. w czasie, gdy w czytelni przebywa pisarz, nie może z niej korzystać inny użytkownik (ani czytelnik, ani inny pisarz). Synchronizacja polega na blokowaniu użytkowników przy wejściu do czytelni, gdy wymaga tego tryb dostępu. shared l_czyt: Integer := 0; shared mutex_r: Binary_Semaphore := true; shared mutex_w: Binary_Semaphore := true; (27) (28) Synchronizacja czytelników i pisarzy za pomocą semaforów binarnych (2) Synchronizacja czytelników i pisarzy za pomocą semaforów binarnych (3) Czytelnik while... do P(mutex_r); l_czyt := l_czyt + 1; if l_czyt = 1 then P(mutex_w); V(mutex_r); czytanie; P(mutex_r); l_czyt := l_czyt - 1; if l_czyt = 0 then V(mutex_w); V(mutex_r); Pisarz while... do P(mutex_w); pisanie; V(mutex_w); (29) (30) 5
Problem pięciu filozofów Synchronizacja 5 filozofów za pomocą semaforów binarnych (1) Przy okrągłym stole siedzi pięciu filozofów, którzy na przemian myślą (filozofują) i jedzą makaron ze wspólnej miski. Żeby coś zjeść, filozof musi zdobyć dwa widelce, z których każdy współdzieli ze swoim sąsiadem. Widelec dostępny jest w trybie wyłącznym może być używany w danej chwili przez jednego filozofa. Należy zsynchronizować filozofów tak, aby każdy mógł się w końcu najeść przy zachowaniu reguł dostępu do widelców oraz przy możliwie dużej przepustowości w spożywaniu posiłków. shared dopuść: Semaphore := 4; shared widelec: array[0..4] of Binary_Semaphore := true; (31) (32) Synchronizacja 5 filozofów za pomocą semaforów binarnych (2) Problem śpiących fryzjerów Filozof nr i (i = 0... 4) while... do myślenie; P(dopuść); P(widelec[i]); P(widelec[(i+1) mod 5]); jedzenie; V(widelec[i]); V(widelec[(i+1) mod 5]); V(dopuść); W salonie fryzjerskim jest poczekalnia z p miejscami oraz n foteli, obsługiwanych przez fryzjerów. Do salonu przychodzi klient, budzi fryzjera, po czym fryzjer znajduje wolny fotel i obsługuje klienta. Jeśli nie ma wolnego fotela, klient zajmuje jedno z wolnych miejsc w poczekalni. Jeśli nie ma miejsca w poczekalni, klient odchodzi. Problem polega na zsynchronizowaniu fryzjerów oraz klientów w taki sposób, aby jeden fryzjer w danej chwili obsługiwał jednego klienta i w tym samym czasie klient był obsługiwany przez jednego fryzjera. (33) (34) Synchronizacja śpiących fryzjerów za pomocą semaforów (1) Synchronizacja śpiących fryzjerów za pomocą semaforów (2) const p: Integer := pojemność poczekalni; const n: Integer := liczba foteli; shared l_czek: Integer := 0; shared mutex: Binary_Semaphore := true; shared klient: Semaphore := 0; shared fryzjer: Semaphore := 0; shared fotel: Semaphore := n; Klient while... do P(mutex); if l_czek < p then l_czek := l_czek + 1; V(klient); V(mutex); P(fryzjer); strzyżenie; end else V(mutex); (35) (36) 6
Synchronizacja śpiących fryzjerów za pomocą semaforów (3) Monitory Fryzjer while... do P(klient); P(fotel); P(mutex); l_czek := l_czek - 1; V(fryzjer); V(mutex); strzyżenie; V(fotel); Monitory jest strukturalnym mechanizmem synchronizacji, którego definicja monitora obejmuje: hermetycznie zamkniętą definicję zamiennych (pól), definicję wejść, czyli procedur umożliwiających wykonywanie operacji na polach monitora. Wewnątrz monitora może być aktywny co najwyżej jeden proces. Proces może zostać uśpiony wewnątrz monitora na zmiennej warunkowej. Uśpiony proces może zostać obudzony przez wysłanie sygnału związanego z daną zmienną warunkową. (37) (38) Ogólny schemat definicji monitora Ograniczony bufor cykliczny definicja oparta na monitorze (1) type nazwa_monitora = monitor deklaracje zmiennych procedure entry proc_1(...);... type Buffer = monitor pula : array [0..n-1] of ElemT; wej, wyj, licz : Integer; pusty, pełny : Condition; procedure entry proc_n(...);... kod inicjalizujący end. (39) (40) Ograniczony bufor cykliczny definicja oparta na monitorze (2) Ograniczony bufor cykliczny definicja oparta na monitorze (3) procedure entry wstaw(elem: ElemT); if licz = n then pełny.wait; pula[wej] := elem; wej := (wej + 1) mod n; licz := licz + 1; pusty.signal; procedure entry pobierz(var elem: ElemT); if licz = 0 then pusty.wait; elem := pula[wyj]; wyj := (wyj + 1) mod n; licz := licz - 1; pełny.signal; (41) (42) 7
Ograniczony bufor cykliczny definicja oparta na monitorze (4) monitora wej := 0; wyj := 0; licz := 0; end. shared buf: Buffer; Producent local elem: ElemType; while... do produkuj(elem); buf.wstaw(elem); Konsument local elem: ElemType; while... do buf.pobierz(elem); konsumuj(elem); (43) (44) Synchronizacja czytelników i pisarzy za pomocą monitora Regiony krytyczne shared czytelnia: Monitor; Czytelnik czytelnia.wejście_czytelnika; czytanie; czytelnia.wyjście_czytelnika; Pisarz czytelnia.wejście_pisarza; pisanie; czytelnia.wyjście_pisarza; Region krytyczny jest fragmentem programu oznaczonym jako S, wykonywanym przy wyłącznym dostępie do pewnej zmiennej współdzielonej, wskazanej w jego definicji oznaczonej jako v. Wykonanie regionu krytycznego uzależnione jest od wyrażenia logicznego B, a przetwarzanie blokowane jest do momentu, aż wyrażenie będzie prawdziwe. shared v: T; region v when B do S; (45) (46) regionu krytycznego (1) regionu krytycznego (2) shared buf: record pula : array [0..n-1] of ElemT; wej, wyj, licz : Integer; Producent local elem: ElemT; while... do produkuj(elem); region buf when buf.licz < n do buf.pula[wej]:= elem; buf.wej := (buf.wej+1) mod n; buf.licz := buf.licz + 1; (47) (48) 8
regionu krytycznego (3) Konsument local elem: ElemT; while... do region buf when buf.licz > 0 do elem := buf.pula[wyj]; buf.wyj := (buf.wyj+1) mod n; buf.licz := buf.licz - 1; konsumuj(elem); (49) 9