Transcript wyklad12

15. MECHANIZMY SYNCHRONIZACJI WĄTKÓW
Większość koncepcji stworzonych na potrzeby synchronizacji procesów ciężkich została zastosowana
też do synchronizacji wątków, ale z pewnymi zmianami. Intencją implementatorów było, aby
biblioteka funkcji obsługujących wątki (w przypadku Linuksa biblioteka pthread) była oddzielnym,
samowystarczalnym narzędziem. Zalecana jest duża ostrożność w przypadku używania w jednym
programie zarówno funkcji mogących blokować procesy, jak i funkcji mogących blokować wątki
(skutki mogą być nieokreślone).
W systemach Linux można spotkać dwie realizacje biblioteki pthread:
- LinuxThreads (starsza, obecnie już nie wspierana);
- NPTL (Native POSIX Thread Library) – oparta na własnościach jądra systemu od 2.6 w górę.
Norma POSIX w odniesieniu do wątków w dużym stopniu wzoruje się na realizacji biblioteki threads
w systemach Solaris firmy Sun.
Obecna realizacja NPTL jest jeszcze dość odległa od zaimplementowania w całości standardu POSIX
(nie są dostępne jeszcze wszystkie przewidziane obiekty synchronizacji wątków). Realizacja funkcji
synchronizujących wątki oparta jest na funkcji systemowej futex( ) (która nie jest jeszcze przenośna na
wszystkie platformy sprzętowe, na których działa Linux).
Wybrane źródła informacji:
man pthreads
(w systemach linuksowych);
http://linux.die.net
(opisy wszystkich funkcji przewidzianych przez POSIX).
Wśród mechanizmów synchronizacji wątków zrealizowanych w systemach Linux należy wymienić:
- wcielenie (join);
- unieważnienie (cancel);
- muteksy (mutex);
- zmienne warunkowe (condition variable);
- obsługę sygnałów (signal) przez wątki;
- semafory (semaphore) POSIX.
Przy okazji omawiania zagadnień synchronizacji wątków na ogół omawiane są również tak zwane
dane specyficzne wątków (thread specific data, TSD), które nie są mechanizmem synchronizacji,
i które stanowią, w pewnym sensie, przeciwieństwo pojęcia pamięci dzielonej dla procesów.
Wśród mechanizmów nie zrealizowanych jeszcze w Linuksach (a zrealizowanych w systemach
Solaris) należy wymienić:
- blokady zapisu/odczytu (read/write lock, RW lock);
- blokady wirujące (spin lock);
- bariery (barrier).
Większość spośród wyżej wymienionych mechanizmów synchronizacji realizowanych jest przez
obiekty synchronizacyjne, które, zgodnie z normą POSIX, powinny być tworzone i usuwane
dynamicznie w programie, i których początkowy stan powinien być określony przez wskazanie na
pewien obiekt atrybutów (podobnie, jak w przypadku samych wątków).
Zgodnie z normą POSIX powinno być możliwe umieszczanie obiektów synchronizacyjnych
w segmentach pamięci wspólnej (w celu synchronizacji wątków niespokrewnionych).
Unieważnienie
Unieważnienie polega na wysłaniu jednemu wątkowi przez drugi żądania zakończenia działania (czyli
przeskoku do funkcji pthread_exit( )). Żądanie takie formalnie nie jest zaliczane do sygnałów, choć ma
podobny charakter (i podobną wewnętrzną realizację) – wątek otrzymuje je asynchronicznie (czyli
w nieprzewidzianym przez siebie momencie).
Ze względu na sposób reagowania na takie żądania każdemu wątkowi przypisany jest stan oraz typ,
które można zmieniać odpowiednimi funkcjami. Są dwa stany:
1) ignorowania żądań (PTHREAD_CANCEL_DISABLE);
2) przyjmowania żądań (PTHREAD_CANCEL_ENABLE).
Jeśli wątek jest w stanie przyjmowania żądań, to typ reakcji może być:
1) natychmiastowy (PTHREAD_CANCEL_ASYNCHRONOUS);
2) opóźniony (PTHREAD_CANCEL_DEFERRED).
Jeśli typ reakcji został ustalony jako opóźniony, to wątek wywołuje funkcję pthread_exit( ) dopiero po
dotarciu do najbliższego (chronologicznie) punktu unieważnienia (cancellation point) w swoim
programie.
Standard POSIX podaje wykaz funkcji (można go znaleźć na przykład w man pthreads), których
miejsca wywołań w programie powinny być uważane za punkty unieważnienia. Zazwyczaj są to funkcje
mogące blokować wątki, na przykład pthread_join( ), pthread_cond_wait( ), pthread_cond_timedwait( ),
sem_wait( ), sigwait( ), ale może też to być nieblokująca funkcja pthread_testcancel( ), jak również
funkcje mogące blokować procesy, takie jak read( ) czy write( ). W powyższym zakresie standard
POSIX prawdopodobnie jeszcze nigdzie nie został zaimplementowany w całości.
Poza wspomnianym wyżej wykazem, POSIX zaleca, aby w przyszłych implementacjach wszystkie
funkcje potencjalnie blokujące były traktowane jako punkty unieważnienia.
int pthread_cancel (pthread_t id);
Zwraca: 0 w przypadku sukcesu
niezerowy kod w przypadku błędu
id – identyfikator wątku
Działanie: wysyła żądanie zakończenia do wątku o identyfikatorze id.
int pthread_setcancelstate (int stan, int *poprzedni_stan);
Zwraca: 0 w przypadku sukcesu
niezerowy kod w przypadku błędu
stan – nowy stan, jaki ma być przyjęty przez wywołujący wątek
poprzedni_stan – jeśli nie jest NULL, to pod tym adresem będzie pamiętany dotychczasowy stan
Działanie: ustala nowy stan wątku, który wywołał tę funkcję (i ewentualnie zapamiętuje
dotychczasowy stan).
int pthread_setcanceltype (int typ, int *poprzedni_typ);
Zwraca: 0 w przypadku sukcesu
niezerowy kod w przypadku błędu
typ – nowy typ reakcji na żądanie zakończenia działania
poprzedni_typ – jeśli nie jest NULL, to pod tym adresem będzie pamiętany dotychczasowy typ
Działanie: ustala nowy typ reakcji wątku, który wywołał tę funkcję (i ewentualnie zapamiętuje
dotychczasowy typ).
void pthread_testcancel (void);
Działanie: ustanawia punkt unieważnienia (i nic więcej).
Sygnały wątkowe
Z powodu wprowadzenia wielowątkowości działanie sygnałów w systemach uniksowych znacznie
skomplikowało się. Jednym ze skutków jest to, że obecnie w programach wielowątkowych nie jest
zalecane używanie tradycyjnej funkcji signal ( ) (jej działanie może dać skutki nieokreślone) – zamiast
niej zalecana jest (zgodna z normą POSIX) funkcja sigaction( ).
W celu implementacji odwzorowania wykonywanych wątków na procesy lekkie jądra systemu
wprowadzono kilka dodatkowych sygnałów (o numerach powyżej 30) – ich bezpośrednie użycie
w programach jest niezalecane.
W przypadku procesów zawierających wiele wątków można rozpatrywać następujące sytuacje:
- wątek sam spowodował konieczność wysłania sygnału przez jądro systemu (na przykład wskutek
próby dzielenia przez 0) – w takim przypadku ten właśnie wątek będzie odbiorcą sygnału;
- wątek wysłał sygnał do wątku spokrewnionego wywołując funkcję pthread_kill ( ) – w takim
przypadku sygnał otrzyma jego adresat;
- do procesu dotarł sygnał asynchroniczny „z zewnątrz”.
W tej ostatniej sytuacji może mieć miejsce:
a) zatrzymanie całego procesu (szczególnie w przypadku sygnału nieprzechwytywalnego SIGKILL);
b) obsługa sygnału przez jeden z wątków procesu (jeśli maski sygnałów umożliwiają to kilku
wątkom, wybór jednego z nich będzie niedeterministyczny).
int pthread_sigmask (int sposób, const sigset_t *nowa_maska, sigset_t *poprzednia_maska);
Zwraca: 0 w przypadku sukcesu;
niezerowy kod w przypadku błędu
sposób – SIG_SETMASK, SIG_BLOCK lub SIG_UNBLOCK
nowa_maska – wskaźnik na wykaz sygnałów
poprzednia_maska - jeśli nie jest NULL, to pod tym adresem będzie pamiętana dotychczasowa maska
Działanie: jeśli sposób jest równy SIG_SETMASK, to ustawia maskę sygnałów wywołującego wątku
dokładnie na podaną, jeśli SIG_BLOCK, to nowe sygnały są dokładane do dotychczasowych,
a jeśli SIG_UNBLOCK, to usuwane spośród dotychczasowych.
int pthread_kill (pthread_t id, int sygnał);
Zwraca: 0 w przypadku sukcesu
niozerowy kod w przypadku błędu
id – identyfikator wątku, do którego wysyłany jest sygnał
sygnał – identyfikator rodzaju sygnału
Działanie: wysyła dany sygnał do danego wątku.
int sigwait (const sigset_t *zbiór, int *sygnał);
Zwraca: zawsze 0
zbiór – wskaźnik na zbiór sygnałów, na jakie wątek ma czekać
sygnał – jeśli nie jest NULL, to pod tym adresem będzie zapamiętany sygnał, który przyjdzie jako
pierwszy z podanego zbioru
Działanie: wywołujący wątek zostaje zawieszony aż do nadejścia dowolnego z sygnałów z podanego
zbioru – wtedy identyfikator tego sygnału zostaje zapamiętany pod adresem podanym przez
drugi argument funkcji. Sygnał musi być uwzględniony w masce sygnałów danego wątku
(nie może być zablokowany). Jeśli z sygnałem związana jest jakaś funkcja jego obsługi
(zarejestrowana dla całego procesu), nie jest ona wykonywana.
Uwaga: Funkcje obsługi sygnałów przychodzących z zewnątrz procesu są zarejestrowane dla całego
procesu (wszystkie wątki procesu je wspóldzielą), ale są wykonywane (w razie potrzeby) przez
pojedynczy wątek. Wątki posiadają swoje indywidualne maski sygnałów. Wątek, który ma
wykonać funkcję obsługi przybyłego sygnału, za każdym razem jest niedeterministycznie
wybierany spośród tych wątków, które w danej chwili mają ten sygnał uwzględniony w swojej
masce.