Wątki

Transkrypt

Wątki
Programowanie
wielowątkowe
Tomasz Borzyszkowski
Wątki a procesy
Jako jeden z niewielu języków programowania Java udostępnia
użytkownikowi mechanizmy wspierające programowanie
wielowątkowe. Program wielowątkowy zawiera kilka równoległych
ścieżek wykonania. Każda tak ścieżka nazywa się wątkiem.
Wielowątkowość jest specyficzną formą wielozadaniowości.
Istnieją dwa typy wielozadaniowości:
 Bazujący na procesach: pozwala na równoległe wykonanie kilku
programów
 Bazujący na wątkach: pojedynczy program może wykonywać kilka
zadań równolegle
Typ procesowy występuje we większości systemów operacyjnych.
Proces posiada bogate środowisko wykonania, swoją przestrzeń
adresową itp. Komunikacja i przełączanie między procesami są
kosztowne.
Wątki są tańsze, ponieważ bazują na jednym procesie i współdzielą
jego zasoby. Pozwalają maksymalnie wykorzystać procesor.
2
Cykl życia wątku
Wątek działa
new Thread
działa
start
wyłączony
sleep
wait
czeka
Koniec
metody run
Powyższy diagram przedstawia cykl życia wątku. Kodowanie
poszczególnych etapów życia wątku przedstawimy w dalszej części
wykładu.
3
Wątki priorytety
Java z każdym wątkiem programu kojarzy priorytet, informujący o tym
jak należy traktować wątek w stosunku do innych wątków programu.
Priorytet jest liczbą całkowitą określającą relatywną ważność wątku w
stosunku do innych wątków.
Priorytet wątku nie ma wpływu na szybkość wykonywania wątku, a
jedynie na pierwszeństwo w wyborze do wykonania (zmiany
kontekstu wykoniania). Reguły rządzące tym kiedy zmieniać kontekst
wykoniania są następujące:
 Wątek może dobrowolnie odstąpić prawo do wykonania innym
wątkom. Sytuacja taka ma miejsce, gdy wątek musi czekać na
zwolnienie zasobu lub, gdy wykonał metodę sleep. Wybierany jest
wówczas wątek z oczekujących o najwyższym priorytecie.
 Wątek może zostać wyparty przez wątek o wyższym priorytecie.
Tj., gdy tylko wątek o wyższym priorytecie potrzebuje procesor, to go
bierze.
4
Wątek główny
Wraz z uruchomieniem programu napisanego w Java jeden wątek,
zwany wątkiem głównym, startuje natychmiast. Wątek główny jest
ważny, ponieważ:
 Jest wątkiem mogącym zapoczątkowywać inne wątki, zwane
wątkami potomnymi
 Często musi być ostatnim wątkiem kończącym wykonanie programu
Wątek główny niczym nie różni się od innych wątków programu.
By uzyskać nad nim kontrolę musimy utworzyć do niego referencję,
korzystając z publicznej statycznej metody currentThread() klasy
Thread. Metoda ta oddaje referencję do wątku, w którym została
wywołana.
Zobacz: CurrentThreadDemo.java
5
Tworzenie własnego wątku
By utworzyć nowy wątek trzeba utworzyć obiekt typu Thread.
Można to zrobić na dwa sposoby:
 Zaimplementować interfejs Runnable
Zobacz:
 Rozszerzyć klasę Thread
NewThreadDemo.java
Implementacja Runnable
Interfejs Runnable jest abstrakcją wykonywalnego kodu.
Można utworzyć wątek bazując na dowolnym obiekcie
implementującym ten interfejs. By implementować ten interfejs, klasa
musi jedynie implementować pojedynczą metodę:
public void run()
Ciało tej metody stanowi kod wątku. Wątek taki kończy się wraz z
zakończeniem działania metody run(). Wywołanie konstruktora:
Thread(Runnable obWątku, String nazwa);
Tworzy nowy wątek bazując na obiekcie obWątku z nazwą nazwa.
Metoda start() uruchamia metodę run() obiektu wątku.
6
Rozszerzanie klasy Thread
Kolejnym sposobem tworzenia własnych wątków jest definiowanie
nowych klas dziedziczących po klasie Thread, następnie tworzenie ich
instancji. Klasa rozszerzająca musi nadpisać metodę run() klasy
Thread. Podobnie jak w poprzednim przykładzie musimy wykonać
metodę start() by rozpocząć wykonanie nowego wątku.
Co lepsze?
Zobacz: ExtendThreadDemo.java
Przedstawiliśmy dwa sposoby tworzenia nowych wątków. Który z nich
jest lepszy? Zgodnie z metodologią obiektową dziedziczenie klas ma
sens, gdy klasa pochodna zmienia coś istotnego w klasie
dziedziczonej. W naszym przypadku była to tylko metoda run().
Wydaje się, że w takim przypadku bardziej odpowiednia jest
implementacja interfejsu Runnable.
Powyższe rozważania traktują raczej o dobrym stylu, niż o
poprawności kodowania.
Zobacz: MultiThreadDemo.java
7
Metody isAlive() i join()
W poprzednich programach staraliśmy się nie doprowadzać do
sytuacji, w której wątek główny kończył by się zanim zakończą się
wątki potomne. Efekt ten osiągaliśmy przez odpowiednio długie
usypianie wątku głównego. Takie rozwiązanie jest rzadko
satysfakcjonujące w realnych programach.
Skąd więc wątek może wiedzieć, że inny wątek się zakończył?
Odpowiedzi na to pytanie mogą udzielić dwie metody klasy Thread:
 final boolean isAlive() oddająca true, gdy wątek, dla
którego ją wywołujemy działa, false w przeciwnym przypadku
 final void join() throws InterruptedException metoda
czeka, aż wątek, dla którego wywołujemy zakończy się; dodatkowe
wersje join pozwalają podać maksymalny czas oczekiwania na
zakończenie wątku
Zobacz: JoinThreadDemo.java
8
Priorytety wątków
Zobacz:
PriorityDemo.java
W praktyce wątki o wyższym priorytecie powinny mieć łatwiejszy dostęp
do procesora niż wątki i niższym priorytecie. W rzeczywistości dostęp
do procesora zależy od wielu czynników. Najważniejszym z nich jest
implementacja w danym systemie wielozadaniowości. Aby zapewnić
wszystkim wątkom dostęp do procesora, także w środowiskach bez
wywałaszczania, wątki powinny oddawać sterowanie co jakiś czas.
Do ustawiania priorytetu wątku służy metoda:
final void setPriority(int poziom)
Parametr poziom, to nowy priorytet wątku. Jego wartości powinny być
z zakresu od MIN_PRIORITY (1) do MAX_PRIORITY (10). Domyślnym
priorytetem jest NORM_PRIORITY (5).
Kompilator optymalizując kod zakłada, że wykonanie bieżącego kodu
zależy jedynie od niego samego. W przypadku wielu wątków często
wątki mogą wpływać na siebie. Słowo volatile wstrzymuje
optymalizację wybranego kodu.
9
Synchronizacja koncepcje
Gdy dwa lub więcej wątków chce korzystać ze wspólnych zasobów,
potrzebują pewnego sposobu upewnienia się, że z zasobu będzie
korzystać tylko jeden wątek w jednym czasie. W informatyce taki sposób
upewniania się nazywa się synchronizacją. Zwykle synchronizacja
wymaga wykonania pewnego protokołu przez wątki. Java dostarcza
własny sposób synchronizacji na poziomie języka.
Synchronizacja jest zapewniana w Java przez tzw. monitory.
Monitor jest obiektem zapewniającym wzajemne wykluczanie wątków.
Tylko jeden wątek może być w posiadaniu jednego monitora w jednym
czasie.
Wątek wchodzący do tzw. sekcji krytycznej otwiera monitor.
Wszystkie inne wątki próbujące otworzyć ten sam monitor są
wstrzymane do czasu aż pierwszy wątek opuści monitor. Wątki takie
nazywamy oczekującymi na dostęp.
Większość języków programowania nie implementuje mechanizmów
wzajemnego wykluczania (patrz C, C++, Pascal, ...).
10
Synchronizacja przykład
Synchronizacja w Java nie jest zbyt skomplikowana. Każdy obiekt w
Java posiada związany z nim niejawny obiekt monitora. Aby otworzyć
monitor obiektu trzeba wywołać jego metodę zdefiniowaną z
modyfikatorem synchronized. Dopóki wątek znajduje się wewnątrz
metody synchronizowanej każdy inny wątek chcący wywołać tę lub
inną metodę synchronizowaną obiektu, musi czekać aż pierwszy wątek
opuści metodę synchronizowaną.
Aby lepiej zrozumieć działanie monitorów w Java, prześledźmy przykład
programu, w którym wątki mogą dostać się do sekcji krytycznej bez
synchronizacji.
Zobacz: NoSynch.java
Poprawka w poprzednim programie polega na synchronizowaniu
metody call() klasy CallMe. Po poprawce tylko jeden wątek będzie
mógł korzystać z metody call() w jednym czasie.
Zobacz: Synch.java
11
Komenda synchronized
Zobacz:
Synchronized.java
Użycie metod synchronizowanych jest prostym sposobem uzyskania
wzajemnego wykluczania wątków. Niestety nie zawsze może być
stosowany.
Wyobraźmy sobie, że chcemy synchronizować dostęp wątków do
obiektu, który nie był zaprojektowany do używania przez wiele wątków.
Tj. klasa ta nie używa metod synchronizowanych. Co więcej klasa ta nie
była projektowana przez nas i nie mamy dostępu do kodu źródłowego.
W jaki sposób możemy synchronizować dostęp do tego obiektu?
Odpowiedź:
Wywołania metod, które powinny być synchronizowane wstawiamy do
bloku synchronized:
synchronized (obiekt) {
// synchronizowane komendy
}
Powyżej obiekt jest synchronizowanym obiektem. Wywołania metod
synchronizowanego obiektu będą możliwe tylko wtedy, gdy obiekt zdoła
otworzyć monitor obiektu.
12
Wątki komunikacja
Dotychczas wątki jedynie blokowały dostęp do zasobów krytycznych.
Po zwolnieniu zasobu wątki konkurują o zwoniony zasób. Polegając na
tym mechaniźmie nie możemy wyrazić bardziej subtelnych związków
między wątkami, takich jak: uczciwość, żywotność, ... .
Java prócz mechanizmów zapewniających wzajemne wykluczanie
oferuje mechanizm wymiany sygnałów między współbieżnymi wątkami.
Służą do tego metody finalne klasy Object: wait(), notify() i
notifyAll(). Metody te mogą być wywołane tylko z metody
synchronizowanej. Ich znaczenie:
 wait() informuje wątek wywołujący by opuścił monitor i zasnął do
czasu aż inny wątek otworzy ten sam monitor i wywoła metodę
notify() lub notifyAll()
 notify() budzi wątek, który jako pierwszy wywołał wait() na tym
samym obiekcie
 notifyAll() budzi wszystkie wątki, które wywołały wait() na tym
samym obiekcie; wątek o najwyższym priorytecie działa, reszta śpi
13
Wątki producent konsument
Produkcja
Producent
Bufor
Konsumpcja
Konsument
Implementacja powyższego modelu producenta i konsumenta z
buforem jednoelementowym wymaga użycia mechanizmu sygnałów.
Niepoprawny sposób implementacji zapewniający wyłącznie wzajemne
wykluczanie w dostępie do bufora można znaleźć w pliku:
ProdKonsErr.java
Poprawną implementację można znaleźć w pliku:
ProdKons.java
Zadanie: zaimplementować model producenta i konsumenta z buforem
wieloelementowym.
14
Wątki zakleszczenie
Istnieje specjalny typ błędu związanego z wielozadaniowością i
wielowątkowością w szególności. Jest nim zakleszczenie.
Z zakleszczeniem mamy do czynienia np. wtedy, gdy jeden wątek
otwiera monitor obiektu X, drugi otwiera monitor obiektu Y, i pierwszy
próbuje wywołać synchronizowaną metodę obiektu Y, natomiast drugi
próbuje wywołać synchronizowaną metodę obiektu X. Oba wątki będą
czekały na zwonienie zasobów blokując je sobie wzajemnie. Taką
sytuację nazywamy zakleszczeniem.
Zakleszczenie jest trudne do wykrycia ponieważ:
 W programie, w którym zakleszczenie jest możliwe zwykle rzadko do
niego dochodzi; oba wątki muszą w tym samym czasie być w
odpowiednim miejscu kodu
 W wielu programach występuje więcej niż dwa wątki i dwie
synchronizowane metody; może dojść do zakleszczeń zespołowych
Zobacz: Zakleszczenie.java
15
Wstrzymaj, wznów, stop
W Java 1.1 wstrzymanie, wznowienie i zatrzymanie wątku odbywało się
przez wywołanie metod suspend(), resume() i stop() klasy
Thread. Metody te nie są stosowane w Java 2.
Zobacz: Suspend1_1.java
W Java 2 zrezygnowano z powyższych metod, ponieważ mogły
doprowadzić do poważnych błędów programu, np.: wstrzymanie wątku,
gdy ten blokuje zasób krytyczny spowoduje zablokowanie tego zasobu
na zawsze, zatrzymanie wątku w trakcie zapisu danych może
spowodować niekompletność zapisywanych danych.
W Java 2 należy tak zaprojektować metodę run() by wątek sam, raz
na jakiś czas, sprawdzał czy powinien wstrzymać, wznowić czy
zatrzymać wykonanie. Zwykle osiąga się to za pomocą flag i metod
wait() i notify().
Zobacz: Suspend2.java
16
Pięciu filozofów
F1
F2
F5
Filozof myśli. Gdy zgłodnieje
podnosi prawy widelec,
następnie lewy i je. Po
zakończonym posiłku odkłada
najpierw prawy a potem lewy
widelec i zaczyna myśleć, aż
zgłodnieje, itd...
Stół
F4
F3
Zadanie:
1. Zaimplementować w Java
problem pięciu filozofów za
pomocą wątków.
2. Rozwiązać zadanie tak by nie
dochodziło do zagłodzenia
żadnego filozofa.
17