wątki










 


Wątki


 
Klasyczny program wykonywany jest
przez procesor komputera w sposób liniowy - instrukcja po instrukcji. W
każdym momencie działania programu wiadomo w którym miejscu znajduje się
sterowanie. Taki program stanowi zatem pojedynczy, sekwencyjny przepływ
sterowania. Programy napisane w języku Java mogą jednak składać się z wielu
przepływów sterowania, zwanych wątkami (thread). Każdy wątek ma początek,
sekwencje instrukcji i koniec. Wątek nie jest niezależnym programem, jest
wykonywany jako część programu. W programie możliwe jest jednocześnie wykonywanie
wielu wątków  i każdy z nich może wykonywać w tym samym czasie odmienne
zadania (tasks).
Program wielowątkwy (multithreaded)
wykonywany na maszynie wieloprocesorowej może wykonywać poszczególne wątki
współbieżnie (concurrent) na różnych procesorach  w tym samym czasie.
W przypadku systemu wyposażonego w  jeden procesor wykonanie programów
wielowątkowych jest tylko emulowane poprzez naprzemienne przydzielanie
czasu procesora poszczególnym wątkom (scheduling) według algorytmu planowania
priorytetowego, tak by sprawiać wrażenie równoległości ich wykonania.
Poszczególnym wątkom mogą być przypisane
różne priorytety, które decydują o kolejności przydzielania im czasu obliczeniowego.
Służy do tego funkcja "setPriority()", której parametr musi mieścić się
w przedziale od MIN_PRIORITY do MAX_PRIORITY (wartości od 1 do 10). Im
większa wartość liczby określającej priorytet, tym priorytet wątku jest
większy. W chwili, gdy wiele wątków jest gotowych do wykonania, wybrany
zostanie wątek z najwyższym priorytetem. Tylko w przypadku  zatrzymania
się wątku, ustępienia czasu procesora (metoda yield) lub przejścia w stan
"nie wykonywany", wątek o niższym priorytecie zaczyna być wykonywany. Gdy
dwa wątki o jednakowym priorytecie czekają na przydzielenie im czasu procesora,
program szeregujący wybiera jeden z nich i przydziela czas według algorytmu
"planowania rotacyjnego" (round-robin). W algorytmie tym ustala się małą
jednostkę czasu, nazywaną kwantem czasu lub odcinkiem czasu. Kolejka procesów
gotowych do wykonania jest traktowana jako kolejka cykliczna. Planista
przydziału procesora przegląda tę kolejkę i każdemu wątkowi przydziela
odcinek czasu nie dłuższy od jednego kwantu czasu. Gdy wątek ma czas wykonania
dłuższy, niż kwant czasu, to nastąpi przerwanie wykonywania wątku i zostanie
on odłożony na koniec kolejki. Wybrany wątek będzie wykonywany do momentu,
gdy jeden z poniższych warunków będzie spełniony: 
    wątek o wyższym
priorytecie przeszedł w stan "wykonywany", 
    wątek udostępnia
procesor lub metoda run kończy działanie, 
    w systemie, w
którym stosuje się przydzielanie kwantów czasu, kwant czasu się skończył. 
Jeśli w jakimś momencie wątek z większym
priorytetem, niż inne wątki w stanie "wykonywany" znajdzie się tym w stanie,
to wybrany zostanie wątek z nowym najwyższym priorytetem. Program szeregujący
wątki może czasani wybrać do wykonania wątek z niższym priorytetem, aby
uniknąć zagłodzenia. 
Istnieją dwie możliwości tworzenia
wątków. Po pierwsze - poprzez utworzenie klasy pochodnej zawierającej kod
wątku od klasy "Thread". Ten sposób daje bezpośredni dostęp do wszystkich
metod kontrolujących wątek zdefiniowanych w klasie Thread. Drugi sposób
polega na implementacji interfejsu Runnable. Ponieważ w Javie nie jest
możliwe wielodziedziczenie, chcąc zdefiniować klasę, która ma własności
wątku i jednocześnie rozszerza właściwości jakiejś innej klasy, należy
skorzystać z implementacji interfejsu Runnable. 
W poniższym przykładzie gry w kości
utworzone zostały dwa typy wątków. "WątekGracza" obrazuje zachowanie graczy
i "CzasGry" odpowiedzialny za odmierzanie czasu trwania gry - z każdą sekundą
będzie zwiększał odpowiedni licznik. Klasa "Gra" zawiera procedurę "main()".
W pierwszej kolejności utworzone zostaną dwa wątki graczy: "Gracz1" i "Gracz2".
Następnie zostaną one uaktywnione (wywołania: "Gracz1.start()" oraz "Gracz1.start()").
Po upływie dziesięciu sekund, gra zostanie zakończona, a otrzymany rezultat
poddany ocenie.
 
public class Gra
{
    static
boolean GraAktywna = true;
    public
static void main (String args[]) throws Exception
    {
       
// tworzenie wątków
       
WatekGracza Gracz1 = new WatekGracza("Gracz1");
       
WatekGracza Gracz2 = new WatekGracza("Gracz2");
       
// ... uruchamianie potoków
       
Gracz1.start();
       
Gracz2.start();
       
// uruchomienie zegara odmierzającego czas trwania gry
       
new CzasGry().start();
       
while (CzasGry.Sekundy < 3);
       
// gra zakończona
       
GraAktywna = false;
       
// podanie wyniku
       
System.out.println("Punkty uzyskane przez Gracza1 : " +
                           
Gracz1.Punkty + " / Gracza2 : " + Gracz2.Punkty);
       
if (Gracz1.Punkty > Gracz2.Punkty)
           
System.out.println("Wygral Gracz1");
       
else
           
System.out.println("Wygral Gracz2");
       
pauza();
    }
    static
void pauza() throws Exception
    {
       
System.out.print("Poprosze Enter ...");
       
System.in.read();
    }
}
class WatekGracza extends
Thread
{
    public
int Punkty;
    //
liczba punktów osiągniętych przez gracza
    public
WatekGracza (String ImieGracza)
    {
       
// konstruktor klasy bazowej wywołany
       
// jest przez nazwę wątku
       
super(ImieGracza);
    }
    public
void run()
    {
       
int Rzut;       // wynik rzutu
       
while (Gra.GraAktywna)
       
{
           
// rzut kośćmi
           
Rzut = (int) (Math.random() * 6);
               
// zwiększenie liczby punktów
               
// o uzyskany wynik rzutu
           
Punkty += Rzut;
           
// podanie wyniku rzutu
           
System.out.println(getName() + " wyrzucił " + Rzut +
                              
" oczek - liczba punktów : " + Punkty);
           
// im gorszy wynik rzutu, tym dłużej musi czekać
           
// gracz na swoją kolej
           
try
           
{
               
sleep ((long) (6-Rzut) * 100);
           
}
           
catch (InterruptedException e)
           
{
               
// wyjątek ten zostanie wywołany kiedy bieżący
               
// potok zostanie przerwany przez inny.
               
// Niniejsza instrukcja catch musi tu zostać
               
// zaimplementowana, w przeciwnym razie
               
// kompilator zgłosi komunikat o błędzie
           
}
       
}
    }
}
class CzasGry extends
Thread
{
    static
int Sekundy = 0;
    public
void run()
    {
       
while (Gra.GraAktywna)
       
{
           
try
           
{
               
sleep (1000);
           
}
           
catch (InterruptedException e)
           
{
           
}
           
Sekundy += 1;
       
}
    }
}
 
 
Metoda "run()" klasy "WątekGracza"
opisuje właściwą część gry: każdy gracz może rzucać kośćmi. Osiągnięta
liczba oczek dodawana jest do ogólnej liczby punktów danego gracza. Im
jest wyższa, tym krócej dany wątek znajduje się w stanie uśpienia (instrukcja
"sleep()" umożliwia ustawienie wątku w stan bezczynności na określony,
podawany w milisekundach czas). W przypadku uzyskania "6" gracz może natychmiast
rzucać dalej. Po dziesięciu sekundach sędzia (funkcja "main()") kończy
grę (ustawiając "GraAktywna = false"). Jeśli utworzonych zostało kilku
graczy, grają oni przeciwko sobie. Ponieważ zależnie od uzyskiwanej liczby
oczek wzajemnie przenoszą się oni w stan uśpienia, każdy z nich, zależnie
od uzyskiwanych rezultatów może rzucać kilka razy pod rząd. Za każdym razem
podawana jest liczba uzyskanych przez gracza oczek oraz ogólna liczba zdobytych
przez niego do tej pory punktów. Użycie funkcji "sleep()" wymusza stworzenie
metody obsługi wyjątku "InterruptedException", ponieważ nie jest to wyjątek
typu "RuntimeException" i jeśli nie zostanie przechwycony interpreter Javy
przerwie wykonanie programu w momencie, w którym się on pojawi. Wyjątek
ten występuje w chwili, gdy jeden wątek jest zatrzymywany, a uaktywniany
inny (przełączenie zadań).
Identyfikacja gracza odbywa się za
pośrednictwem nazwy odpowiadającego mu wątku. Konstruktor klasy "WatekGrasza"
otrzymuje tę nazwę jako parametr. Za pośrednictwem wywołania "super()"
przekazuje ją dalej, do konstruktora klasy bazowej. Z kolei "getName()"
podaje nazwę wątku, który jest aktualnie aktywny.
 
 
Przykład: Implementacja interfejsu
Runnable
class Watek implements
Runnable
{
     
// W polu danych biezacy przechowywana będzie referencja do wątku,
     
// w którym wykonana zostanie klasa WatekRunnable
    Thread
biezacy;
    int
n;
    public
Watek( String name, int number)
    {
         
// metoda statyczna currentThread() klasy Thread zwaca
         
// referencję do bieżącego wątku
       
biezacy = Thread.currentThread();
       
biezacy.setName ( name );
       
this.n = number;
    }
    public
void run()
    {
         
// dzięki referencji biezacy można na rzecz tego
         
// wątku wykonać metodę getName() (z klasy Thread)
       
System.out.println( biezacy.getName() + " - czas trwania "
                            
+ this.n + "s");
       
try
       
{
           
biezacy.sleep(n * 1000);
       
}
       
catch (InterruptedException e) {}
       
System.out.println( biezacy.getName()+ " zakonczyl dzialanie" );
    }
}
 
 
public class WatekRunnable
{
     
// deklaracja  wątku, jest jako static, bo
     
// tylko do pól statycznych klasy można się odwołać w statycznej
     
// metodzie main()
    static
Thread watek1;
    static
Thread glowny;
    public
static void main (String[] args) throws Exception
    {
         
// przypisanie do pola danych watek  referencji
         
// do obiektów typu Thread
       
watek1 = new Thread();
         
// utworzenie nowego wątku
       
watek1 = new Thread( new WatekPodstawowy("Watek pierwszy", 5));
         
// uruchomienie wątku
       
watek1.start();
       
System.out.println("Watek glowny - czas trwania 3s");
       
glowny.sleep(3000);
       
System.out.println ("Watek glowny zakonczyl dzialanie");
       
Enter();
    }
    static
void Enter() throws Exception
    {
       
System.out.print("Poprosze Enter.....");
       
System.in.read();
    }
}
 
 
Wykonanie metody start (public synchronized
void start()) na rzecz utworzonego wcześniej obiektu klasy Thread powoduje
utworzenie wątku. Przebieg wykonania wątku zależy od implementacji metody
run (public void run()), wywoływanej niejawnie przez system tuż po utworzeniu
wątku. Zakończenie wykonania metody run() powoduje niejawne wywołanie metody
stop (public static final void stop()), która niszczy wątek. Stanami wątków
można sterować przez wywołania finalnych metod wystąpienia.
 
 
W czasie swego istnienia wątek może
znajdować się w jednym z kilku stanów:
 
    nowy wątek
Tworzenie nowego wątku bez uruchamia
go, pozostawienie wątku w stanie "nowy wątek". 
Thread Watek = new KlasaWatku();
Po wykonaniu tej instrukcji worzonyjest
pusty obiekt Thread. Zasoby systemowe nie zostały jeszcze przydzielone
dla wątku. Kiedy wątek znajduje się w tym stanie, można tylko wykonać metodę
start, uruchamiającą wątek, lub stop, kończącą działania wątku. Wywołanie
innych metod dla wątków w tym stanie powoduje wystąpienie wyjątku IllegalThreadStateException. 
    wykonywany
 
Thread Watek = new KlasaWatku();
mojWatek.start();
Metoda start tworzy zasoby systemowe
potrzebne do wykonania wątku, przygotowuje wątek do uruchomienia, oraz
wywołuje metodę run. Od tego momentu wątek jest w stanie "wykonywany". 
    nie wykonywany 
Wątek przechodzi w stan "nie wykonywany"
gdy zachodzi  zdarzenie: 
    wywołano jego
metodę sleep, 
    wywołano jego
metodę suspend, 
    wątek wykonuje
swoją metodę wait, 
    wątek jest zablokowany
przy operacji wejścia / wyjścia (ang. I/O). 
Np: wykonanie metody sleep(10000)
powoduje uśpienie bieżącego wątku na 10 sekund 
try 
{
    Thread.sleep(10000);
}
catch (InterruptedException
e)
{ }
Gdy wątek jest uśpiony, nawet gdy
procesor staje się dostępny dla tego wątku, wątek ten nie zostaje uruchomiony.
Po upływie określonego w metodzie sleep czasu wątek przechodzi do stanu
"wykonywany" i jeśli procesor jest wolny, jest on uruchamiany. Dla każdego
przypadku przejścia wątku do stanu "nie wykonywany" istnieją specyficzne
warunki, jakie muszą być spełnione, aby nastąpił powrót do stanu "wykonywany".
Są to:
    wątek uśpiono
(sleep ), musi upłynąć określona liczba milisekund, 
    wątek zawieszono
(suspend), inny wątek musi wywołać metodę resume wątku, powodującą jego
odwieszenie, 
    wątek czeka na
np. ustawienie jakiejś zmiennej - obiekt, do którego należy ta zmienna,
musi ją odstąpić a następnie wywołać metodę notify lub notifyAll, 
    jeśli wątek jest
zablokowany przy operacjach wejścia / wyjścia, wtedy operacje te muszą
być zakończone. 
 
 
    zakończony
Wątek może zakończyć swoje działanie
naturalnie, gdy jego metoda run kończy się normalnie, albo gdy zostanie
zabity (kill) po wykonaniu metody stop. 
public void run()     
//  normalne zakończenie 
{
    int
n = 0;
    while
(n < 10) 
    {
       
System.out.println( i + " iteracja");
       
i++;
    }
}
 
 
Thread Watek = new KlasaWatku();
Watek.start();   
//  zakończenie poprzez wywołanie metody stop
try 

    Thread.sleep(1000);
}
catch (InterruptedException
e)
{}
mojWatek.stop();
 
Metoda stop oznacza nagłe zakończenie
wykonania metody run wątku. W przypadku gdy metoda run wykonuje jakieś
ważne obliczenia, wywołanie metody stop może spowodować przerwanie wykonywania
programu w stanie niespójnym. Dlatego nie powinno się wołać metody stop
wtedy, gdy chcemy zakończyć wątek, lecz  poprzez ustawienie flagi
informującej metodę run, że powinna zakończyć swoje wykonanie. 
 
 
Metoda isAlive 
Wynikiem metody isAlive jest wartość
true, gdy wątek został uruchomiony a nie  zakończył jeszcze swojego
działania. Gdy metoda isAlive zwraca wartość false oznacza to, że wątek
jest albo w stanie "nowy wątek" lub "zakończony". Gdy wynikiem jest wartość
true wiadomo, że jest albo w stanie "wykonywany" lub "nie wykonywany". 
 
Metoda yield()
oddaje dostęp do procesora innemu
wątkowi (o ile taki istnieje).
 
 
Metoda join() i join(long millisec)
powoduje wstrzymanie wykonywania
wątku aż do zniszczenia go przez inny wątek
 
 
Metody notifyAll() i wait()
Mogą być wywoływane tylko przez wątki,
które założyły blokadę. Metoda notifyAll informuje wszystkie wątki oczekujące
na monitor (blokada zakładana na dane współdzielone przez kilka wątków)
zajęty przez bieżący wątek o zwolnieniu tego monitora i budzi te wątki.
Przeważnie jeden z oczekujących wątków zajmuje monitor i wykonuje swoje
zadanie. 
 
 
 
Wyjątek IllegalThreadStateException 
wyjątek IllegalThreadStateException
jest generowany gdy nastąpiło wywołanie metody wątku  znajdujcego
się w  stanie, który nie pozwala na wywołanie tej metody. Przykładowo
w stanie "nie wykonywany" wyjątek ten występuje, gdy próbuje się wywołać
metodę suspend.
 
 
Synchronizacja wątków
 
 
Czasami może zaistnieć sytuacja,
gdzie oddzielne, współbieżnie wykonywane wątki współdzielą pewne dane i
muszą uwzględniać stany i aktywności innych wątków. Powszechnie znanym
modelem programistycznym takich sytuacji jest model producent/konsument,
w którym producent generuje strumień danych pobieranych następnie przez
konsumenta. Przykładem praktycznym może być program, w którym jeden wątek
(producent) zapisuje dane do pliku, podczas gdy drugi wątek czyta dane
z tego samego pliku. Podane przykłady wykorzystuje współbieżne wątki, które
dzielą wspólny zasób. Ponieważ wątki współdzielą wspólny zasób, muszą być
w jakiś sposób synchronizowane. Segmenty kodu programu, które żądają dostępu
do tego samego obiektu z dwóch oddzielnych współbieżnych wątków, nazywa
się sekcjami krytycznymi. W języku Java sekcją krytyczną może być blok
lub metoda; identyfikuje się je słowem kluczowym synchronized.
W przypadku bloku instrukcja synchronized
ma składnie
synchronized ( Wyrażenie ) Blok
w której Wyrażenie musi być typu
referencyjnego, a Blok jest instrukcją grupującą, objetą nawiasami klamrowymi.
Instrukcja synchronized przejmuje
wzajemnie wykluczającą blokadę na rzecz wykonywanego wątku, wykonuje Blok,
po czym zwalnia blokadę. Inaczej mówiąc, wykonanie instrukcji synchronized
powoduje przydzielenie wątkowi podanego Bloku jako sekcji krytycznej, a
po wykonaniu go na rzecz obiektu identyfikowanego przez Wyrażenie, zwolnienie
sekcji.
Metoda jest synchronizowana jeżeli
w jej nagłówku umieszczono słowo kluczowe synchronized. Synchronizowana
metoda operująca na obiekcie pewnej klasy automatycznie nakłada blokadę
na ten obiekt przed wykonaniem jego ciała (funkcji, metod) i automatycznie
zwalnia blokadę przy powrocie, podobnie jak instrukcja synchronized. 
Wymieniona blokada jest w programie
wielowątkowym związana z obiektem, na którym wątek ma wykonywać pewne operacje.
Jest ona często nazywana monitorem obiektu. Do czasu zwolnienia monitora,
zostanie zablokowane wykonanie każdego innego wątku, który podejmie próbę
wywołania (na rzecz tego samego obiektu), dowolnej metody sychronizowanej
danej klasy.
W Javie każdy obiekt, który ma metody
synchroniczne posiada swój monitor. Klasa Produkt ma dwie metody synchroniczne:
metodę produkuj, używaną do zmiany wartości w obiekcie typu Produkt i metodę
konsumuj, która jest używana do pobrania liczby przechowywanej w obiekcie
Produkt. Oznacza to, że system skojarzy z każdym obiektem typu Produkt
unikalny monitor. 
 
 
class Producent extends
Thread
{
    private
Produkt produkt;
    private
int Liczba;
    public
Producent(Produkt c, int liczba)
    {
       
produkt = c;
       
this.Liczba = liczba;
    }
    public
void run()
    {
       
for (int i = 0; i < 5; i++)
       
{
           
produkt.produkuj(i);
           
System.out.println("Producent #" + this.Liczba + " wyprodukowal: " + i);
           
try
           
{
               
sleep((int)(Math.random() * 500));
           
}
           
catch (InterruptedException e) {  }
       
}
    }
}
 
class Konsument extends
Thread
{
    private
Produkt produkt;
    private
int Liczba;
    public
Konsument(Produkt c, int Liczba)
    {
       
produkt = c;
       
this.Liczba = Liczba;
    }
    public
void run()
    {
       
int wartosc = 0;
       
for (int i = 0; i < 5; i++)
       
{
           
wartosc = produkt.komsumuj();
           
System.out.println("Konsument #" + this.Liczba + " wyjal: " + wartosc);
       
}
    }
}
 
class Produkt
{
    private
int Zawartosc;
    private
boolean Dostepne = false;
    public
synchronized int komsumuj()
    {
       
// monitor zostaje zajęty przez Konsumenta
       
while (Dostepne == false)
       
{
           
try
           
{
               
wait(); } // metoda wait() tymczasowo zwalnia monitor,
           
)
           
catch (InterruptedException e) { }
       
}
       
Dostepne = false;
       
notifyAll();
       
return Zawartosc;
       
// monitor zostaje zwolniony przez Konsumenta
    }
    public
synchronized void produkuj(int wartosc)
    {
       
// monitor zostaje zajęty przez Producenta
       
while (Dostepne == true)
       
{
           
try
           
{
               
wait();
           
} // metoda wait() tymczasowo zwalnia monitor
           
catch (InterruptedException e) { }
       
}
       
Zawartosc = wartosc;
       
Dostepne = true;
       
notifyAll();
       
// monitor zostaje zwolniony przez Producenta
    }
}
 
class ProducentKonsument
{
    public
static void main(String[] args) throws Exception
    {
       
Produkt X = new Produkt();
       
Producent Prod = new Producent(X, 1);
       
Konsument Kons = new Konsument(X, 1);
       
Prod.start();
       
Kons.start();
       
Enter();
    
}
    
static void Enter() throws java.io.IOException
    
{
        
System.out.println("Poprosze Enter ...");
        
System.in.read();
    
}
}
 
 
Klasa Produkt posiada dwa pola danych:
Zawartosc, które stanowi bieżącą zawartość pudełka oraz pole danych Dostepne
typu boolean, które określa, czy zawartość produktu może być pobrana. Gdy
zmienna Dostepne jest równa true oznacza to, że Producent właśnie umieścił
nową wartość w Produkcie, a Konsument jeszcze jej nie pobrał. Konsument
może pobrać daną z Produktu tylko wtedy, gdy zmienna Dostępne jest równa
true. 
Jeśli usunie się z metod komsumuj
i produkuj klasy Produkt elementy kodu odpowiedzialne za synchronizacje
Konsument będzie pobierał liczby z Pudełka nie czekając na wyprodukowanie
przez Producenta kolejnej liczby. 
Ponieważ klasa Produkt ma dwie metody
synchroniczne, dla każdego obiektu klasy Produkt tworzony jest oddzielny
monitor. W momencie gdy sterowanie znajdzie się w metodzie synchronicznej,
wątek, który wywołał tę metodę zajmuje monitor obiektu, którego metodę
wywołano. Inne wątki nie mogą wołać metod synchronicznych tego obiektu
do czasu zwolnienia monitora. Monitory w Javie są wielodostępne. Oznacza
to, że wątek, który zajął monitor jakiegoś obiektu może wołać inne metody
synchroniczne obiektu. 
Zawsze wtedy, gdy Producent woła
metodę konsumuj, Producent zajmuje monitor obiektu Produkt, co powoduje,
że Konsument nie może wywołać metody konsumuj do czasu zwolnienia monitora.
Gdy metoda produkuj kończy działanie, Producent zwalnia monitor i odblokowuje
obiekt typu Produkt. 
Podobnie gdy Konsument woła metodę
konsumuj klasy Produkt, Konsument zajmuje monitor obiektu typu Produkt,
co powoduje, że Producent nie może wywołać metody produkuj do czasu zwolnienia
monitora. 
Operacje zajmowania i zwalniania
monitora są wykonywane automatycznie przez środowisko wykonawcze Javy,
co zapewnia integralność danych i chroni przed wystąpieniem sytuacji wyjątkowych
spowodowanych operacjami na monitorach. 







Wyszukiwarka

Podobne podstrony:
wątki ceglane pśin
SO 05 Watki
watki
watki
opis watki (2)
Wątki mitologiczne w sztuce nowożytnej
opis watki
watki
Java watki
sołtys,systemy operacyjne, wątki
wyk watki mutexy
watki 2
Wątki
Wątki
watki

więcej podobnych podstron