4. Wprowadzenie do programowania współbieżnego w Javie
4.1. Podstawowe pojęcia: procesy i wątki
Proces - to wykonujący się program wraz z dynamicznie przydzielanymi
mu przez system zasobami (np. pamięcią operacyjną, zasobami plikowymi) oraz,
ewentualnie, innymi kontekstami wykonania programu (np. obiektami tworzonymi przez program) .
Systemy wielozadaniowe pozwalają na (praktycznie) równoległe wykonywanie wielu procesów, z których każdy ma swój kontekst, w tym: swoje zasoby.
W systemach wielozadaniowych i wielowątkowych - jednostką wykonawczą procesu jest wątek.
Każdy proces ma co najmniej jeden wykonujący się wątek, ale może też mieć ich wiele.
Proces "posiada" zasoby i inne konteksty, wykonaniem "zadań" procesu zajmują
się wątki (swego rodzaju podprocesy, wykonujące różne działania w kontekście
jednego procesu).
Wątki działają (praktycznie) równolegle.
Zatem równoległość działań w ramach procesu (jednego programu) osiągamy przez uruchamianie kilku różnych wątków.
Wątek - to sekwencja działań, która może wykonywać się równolegle z innymi sekwencjami działań w kontekście danego procesu
(programu).
Dodatkowych wątków w ramach jednego
programu używa się często (choć nie zawsze) do wykonania czynności, które
zajmują sporo czasu i przez to mogą blokować działanie całego programu. Gdy
zostaną one wyodrębnione jako wątki, działanie aplikacji nie będzie wstrzymywane,
a czasochłonne czynności wykonywane są (praktycznie) równolegle z innymi
(może podstawowymi) działaniami aplikacji, niejako w tle.
Co tak naprtawdę oznacza równoległość wykonywania wątków?
Przecież w każdym momencie czasu procesor może wykonywać tylko jakąś jedną instrukcję.
Zatem w systemach jednoprocesorowych tak naprawdę w każdym momencie czasu
wykonuje się tylko jeden wątek i nie ma tu "prawdziwej" równoległości.
Wrażenie rownoległości dzialania wątków osiągane jest przez mechanizm przydzielania
czasu procesora poszczególnym wykonującym się wątkom. Każdy wątek uzyskuje
dostęp do procesora na krótki czas (kwant czasu), po czym "oddaje procesor"
innemu wątkowi. Zmiany są tak szybkie, że powstaje wrażenie równoleglości
działania.
Ponieważ Java jest językiem wieloplatformowym, a różne systemy operacyjne
stosują różne mechanizmy udostępniania wątkom czasu procesora, pisząc programy
wielowątkowe w Javie powinniśmy zakladać, że mogą one działac zarówno w środowisku
"współpracy", jak i "konkurencji" (mechanizm wywłaszczania)
Zmiany wątków
"u procesora" mogą dokonywać się wedle dwóch mechanizmów:
- współpracy (cooperative multistasking) - wątek sam decyduje, kiedy oddać czas procesora innym wątkom,
- wywłaszczania (pre-emptive multistasking) - o dostępie
wątków do procesora decyduje systemowy zarządca wątków: przydziela on wątkowi
kwant czasu procesora, po upłynięciu którego odsuwa wątek od procesora i
przydziela kwant czasu procesora innemu wątkowi.
Proces wykonuje się poprzez wykonanie jego wątków.
Zatem, zwolnienie procesora przez wątek jednego procesu i przydzielenie procesora
wątkowi innego procesu wymaga "przeładowania" kontekstu procesu, bowiem
każdy proces ma swój niezależny kontekst (np. przestrzeń adresową, odniesienia
do otwartych plików).
Podstawowa róznica pomiędzy procesami i wątkami polega na tym, że różne wątki
w ramach jednego procesu mają dostęp do całego kontekstu tego procesu (m.in.
przydzielonych mu zasobów).
Wobec tego zamiana wątków jednego procesu "przy procesorze" jest wykonywana
szybciej niż zamiana procesów (wątków różnych procesów).
Z punktu widzenia programisty wspólny dostęp wszystkich wątków jednego procesu
do kontekstu tego procesu ma zarówno zalety jak i wady.
Zaletą jest możliwość łatwego dostępu do wspólnych danych programu. Wadą
- brak ochrony danych programu przed równoległymi zmianami, dokonywanymi
przez różne wątki, co może prowadzić do niespójności danych, a czego unikanie
wiąże się z koniecznością synchronizacji dzialania wątków.
4.2. Jak stworzyć i uruchomić nowy wątek?
Uruchamianiem wątków i zarządzaniem nimi zajmuje się klasa Thread.
Aby uruchomić wątek należy stworzyć obiekt klasy Thread i użyć metody start() wobec tego obiektu.
Ale kod, wykonujący się jako wątek (sekwencja działań, wykonująca się równolegle
z innymi działaniami programu) określany jest przez obiekt klasy implementującej interfejs
Runnable.
Interfejs ten zawiera deklarację metody run(), która przy implementacji musi być zdefiniowana.
Właśnie w metodzie run() zapisujemy kod, który będzie wykonywany jako wątek (równolegle z innymi wątkami programu).
Metoda run() określa co ma robić wątek.
Klasa Thread implementuje interfejs Runnable (podając "pustą" metodę run).
Stąd pierwszy sposób tworzenia i uruchamiania wątku.
Pierwszy sposób tworzenia i uruchamiania wątku
|
- Zdefiniować własną klasę dziedziczącą Thread (np. class Timer extends Thread)
-
Przedefiniować odziedziczoną metodą run(), podając w niej działania, które ma wykonywać wątek
-
Stworzyć obiekt naszej klasy (np. Timer timer = new Timer(...);
-
Wysłać mu komunikat start() (np. timer.start())
|
Niech klasa Timer służy do zliczania czasu. Może wyglądać tak.
public class Timer extends Thread {
public void run() {
int time = 0;
while (true) {
try {
this.sleep(1000);
} catch(InterruptedException exc) {
System.out.println("Wątek zliczania czasu zoostał przerwany.");
return;
}
time++;
int minutes = time/60;
int sec = time%60;
System.out.println(minutes + ":" + sec);
}
}
}
Metoda sleep może sygnalizować wyjątek InterruptedException, który powstaje
na skutek przerwania działania wątku (np. przez zniecierpliowanego długim
czekaniem na jego obudzenie użytkownika). Dlatego musimy obsługiwać ten wyjątek.
Licznik czasu możemy zastosować np. w następującym programie, który wymaga
od użytkownika podania wszystkich stolic (lądowych) sąsiadów Polski.
import javax.swing.*;
public class Quiz {
// Stolice do odgadnięcia
private final String[] CAP = {"Praga", "Bratysława", "Moskwa",
"Berlin", "Kijów", "Wilno", "Mińsk" };
// Czy stolica była już podana ?
private boolean[] entered = new boolean[CAP.length];
public Quiz() {
int n = CAP.length;
JOptionPane.showMessageDialog(null, "Podaj stolice lądowych sąsiadów Polski");
String askMsg = "Wpisz kolejną stolicę:" ;
int count = 0; // ile podano prawidłowych odpowiedzi
// Uruchomienie wątku zliczającego i pokazującego upływający czas
Timer tm = new Timer();
tm.start();
while (count < CAP.length) { // dopóki nie podano wszystkich stolic
String input = JOptionPane.showInputDialog("Odpowiedzi: " + count + '/' + n +
'\n' + askMsg);
if (input == null) break;
if (isOk(input)) count++; // jeżeli ta odpowiedź prawidłowa
}
System.exit(0);
}
// Czy odpowiedź jest prawidłowa i czy jej wcześniej nie podano?
private boolean isOk(String s) {
for (int i=0; i < CAP.length; i++) {
if (s.equalsIgnoreCase(CAP[i]) && !entered[i])
return (entered[i] = true);
}
return false;
}
public static void main(String args[]) {
new Quiz();
}
}
Nasz program równolegle wykonuje dwa zadania: interakcję z użytkownikiem
(pytania o stolice) oraz wypisywanie na konsoli informacji o upływającym
czasie.
Oczywiście, nic nie stoi na przeszkodzie, by odziedziczyć klasę Thread w
klasie Quiz (pytającej użytkownika o stolice). Przy okazji zobaczymy, że
dwa wątki mogą odwoływać się do wspólnych danych.
import javax.swing.*;
public class Quiz1a extends Thread {
private final String[] CAP = {"Praga", "Bratysława", "Moskwa",
"Berlin", "Kijów", "Wilno", "Mińsk" };
private boolean[] entered = new boolean[CAP.length];
private int time = 0; // licznik czasu
public Quiz1a() {
int n = CAP.length;
JOptionPane.showMessageDialog(null, "Podaj stolice sąsiadujących krajów");
String askMsg = "Wpisz kolejną stolicę:" ;
int count = 0;
// Uruchomienie wątku zliczania czasu
start();
while (count < CAP.length) {
String input = JOptionPane.showInputDialog("Odpowiedzi: " + count + '/' + n +
". Czas: " + getTime() + '\n' +
askMsg);
if (input == null) break;
if (isOk(input)) count++;
}
JOptionPane.showMessageDialog(null, "Czas wpisywania: " + getTime());
System.exit(0);
}
// Kod który wykonuje się w odrębnym wątku
public void run() {
while (true) {
try {
this.sleep(1000);
} catch(InterruptedException exc) {
System.out.println("Wątek zliczania czasu został przerwany.");
return;
}
time++;
System.out.println(getTime());
}
}
// Metoda zwracająca bieżący czas w formie min : sek
private String getTime() {
int minutes = time/60;
int sec = time%60;
return minutes + ":" + sec;
}
private boolean isOk(String s) {
for (int i=0; i < CAP.length; i++) {
if (s.equalsIgnoreCase(CAP[i]) && !entered[i])
return (entered[i] = true);
}
return false;
}
public static void main(String args[]) {
new Quiz1a();
}
}
Problemy z dostępme do tej samej zmiennej!
Jak już wiemy, kod wykonywany przez wątek podajemy w metodzie run(). A
metoda run() może być zdefiniowana w dowolnej klasie implementującej interfejs
Runnable.
Klasa Thread dostarcza zaś konstruktora, którego argument jest typu Runnnable.
Konstruktor ten tworzy wątek, który będzie wykonywał kod zapisany w metodzie
run() w klasie obiektu, do którego referencję przekazano wspomnianemu wyżej
konstruktorowi.
Stąd drugi sposób tworzenia i uruchamiania wątków.
Drugi sposób tworzenia i uruchamiania wątku
|
- Zdefiniować klasę implementującą interfejs Runnable (np. class X implements Runnable).
- Dostarczyć w niej definicji metody run (co ma robić wątek).
- Utworzyć obiekt tej klasy (np. X x = new X(); )
- Utworzyć obiekt klasy Thread, przekazując w konstruktorze referencję
do obiektu utworzonego w p.3 (np.Thread thread = new Thread(x);).
- Wywołać na rzecz nowoutworzonego obiektu klasy Thread metodę start ( thread.start();)
|
Oprogramowanie uprzednio omawianego licznika czasu przy użyciu drugiego sposobu
tworzenia i uruchamiania watków wyglądałoby tak:
class Timer implements Runnable {
public void run() {
int time = 0;
while (true) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) {
System.out.println("Wątek zliczania czasu zoostał przerwany.");
return;
}
time++;
int minutes = time/60;
int sec = time%60;
System.out.println(minutes + ":" + sec);
}
}
}
a utworzenie i uruchomienie wątku zliczającego czas (w innej klasie) w następujący sposób:
Timer timer = new Timer();
Thread thread = new Thread(timer);
thread.start();
lub zwięźlej:
new Thread(new Timer()).start();
Drugi sposób tworzenia i uruchamiania wątków ma pewne zalety w stosunku do
korzystania wyłącznie z klasy Thread (czyli omówionego wcześniej sposobu
pierwszego):
- niekiedy daje lepsze możliwości separowania kodu (kod odpowiedzialny za pracę wątku może być wyraźnie wyodrębniony w klasie implementującej Runnable).
- a w niektórych okolicznościach - mianowicie, gdy chcemy umieścić
metodę run() w klasie, która dziedziczy jakąś inną klasę - jest jedynym możliwym
sposobem.
Wyobraźmy sobie oto, że w znanym nam już przykładzie z samochodami (zużycie
paliwa w czasie jazdy - zob. klasa Car i materiał o klasach wewnętrznych w poprzednim wykładzie) nie dysponujemy
możliwością uzycia klasy Timer z pakietu javax.swing.
Sami musimy oprogramować symulację zużycia paliwa.
Jak przedtem, umówimy się, że w każdej sekundzie czasu programu zużywany jest 1 litr paliwa.
Klasa Car dziedziczy klasę Vehicle, zatem nie możemy już odziedziczyć klasy
Thread. Ale możemy implementować interfejs Runnable i w klasie Car dostarczyć
metody run(), która będzie symulować zużycie paliwa.
public class Car extends Vehicle implements Runnable {
private String nrRej;
private int tankCapacity; // pojemność baku
private int fuel; // ile jest paliwa?
public Car(String nr, Person owner, int w, int h, int l,
int weight, int tankCap) {
super(owner, w, h, l, weight);
nrRej = nr;
tankCapacity = tankCap;
}
// Napełnianie baku
public void fill(int amount) {
fuel += amount;
if (fuel > tankCapacity) fuel = tankCapacity;
}
// Start samochodu
public void start() {
if (fuel > 0) {
super.start();
new Thread(this).start(); // uruchamiamy wątek zużycia paliwa
}
else System.out.println("Brak paliwa");
}
// Zatrzymanie samochodu
public void stop() {
super.stop();
}
// Kod, który wykonuje się w odrębnym wątku
// co 1 sek. czasu programu zużywany jest 1 litr paliwa
public void run() {
while(true) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
fuel--;
System.out.println("Paliwo: " + fuel); // śledzimy ile jest paliwa
if (fuel <= 0) break; // jeżeli brak paliwa...
}
System.out.println("Zatrzymanie samochodu z powodu braku paliwa");
stop(); // zatrzymanie samochodu, bo brak paliwa
}
public String toString() {
return "Samochód nr rej " + nrRej + " - " + getState(getState());
}
}
W klasie TestCar1 przetestujemy działanie programu na przykładzie jednego samochodu:
public class TestCar1 {
// Symulacja upływu czasu...
static void delay(int sek) {
while(sek-- > 0) {
try {
Thread.sleep(1000);
} catch (Exception exc) { }
}
}
public static void main(String[] args) {
Car c = new Car("WA1090", new Person("Janek", "0909090"),
100, 100, 100, 100, 50);
c.fill(10); // napełniamy bak
c.start(); // ruszamy ...
System.out.println(c + ""); // co się dzieje z samochodem
delay(12); // niech upłynie 12 sek. jazdy od tego momentu
System.out.println(c + ""); // co się dzieje z samochodem
}
}
Wyniki dzialania programu przedstawia wydruk.
Samochód nr rej WA1090 - JEDZIE
Paliwo: 9
Paliwo: 8
Paliwo: 7
Paliwo: 6
Paliwo: 5
Paliwo: 4
Paliwo: 3
Paliwo: 2
Paliwo: 1
Paliwo: 0
Zatrzymanie samochodu z powodu braku paliwa
Samochód nr rej WA1090 - STOI
Wydaje się, że wszystko jest w porządku. Ale co by się stało gdyby po upływie 3 sekund jazdy zatrzymać samochód?
c.fill(10); // napełniamy bak
c.start(); // ruszamy ...
System.out.println(c + ""); // co się dzieje z samochodem
delay(3); // niech upłyną 3 sek.
c.stop(); // samochód się zatrzymuje...
System.out.println(c + ""); // co się dzieje z samochodem
delay(9); // niech upłynie jeszcze 9 sek. od tego momentu
System.out.println(c + ""); // co się dzieje z samochodem
Wynik nie będzie właściwy, po zatrzymaniu samochodu nadal będzie zużywane paliwo:
Samochód nr rej WA1090 - JEDZIE
Paliwo: 9
Paliwo: 8
Samochód nr rej WA1090 - STOI
Paliwo: 7
Paliwo: 6
Paliwo: 5
Paliwo: 4
Paliwo: 3
Paliwo: 2
Paliwo: 1
Paliwo: 0
Zatrzymanie samochodu z powodu braku paliwa
Nie jest mozliwe przejscie ze stanu STOI do stanu STOI
Samochód nr rej WA1090 - STOI
Dzieje się tak dlatego, że mimo zatrzymania samochodu (metodą stop()),
wątek zużycia paliwa nadal dziala. Tajemniczy komunikat "Nie jest mozliwe
przejscie ze stanu STOI do stanu STOI" pojawia się na skutek użycia metody
stop() (po wyczerpaniu paliwa, w metodzie run()) wobec pojazdu, który stoi
(komunikat jest generowany przez klasę Vehicle).
Niewątpliwie, w momencie zatrzymania samochodu metodą stop() należy zakończyć
działanie wątku zużycia paliwa. Musimy zatem mieć jakiś sposób na kończenie
działania wątków (o czym w następnym piunkcie).
Warto jeszcze podkreślić, że kod, który ma wykonywać się jako odrębny wątek możemy dostarczyć
w klasie wewnętrznej (por. wykład 3).
Przy tworzeniu wątków ad hoc (zwykle tylko raz i na jakąś konkretną potrzebę)
bardzo często posługujemy się anonimowymi klasami wewnętrznymi i to zwykle
lokalnymi.
Na przykład - do zliczania i pokazywania upływu czasu w trakcie jakiejś
interakcji użytkownika z programem, jak w poniższym programie:
import javax.swing.*;
class AnonymousRunnable {
public AnonymousRunnable() {
Runnable runner = new Runnable() {
public void run() {
int time = 0;
while (true) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
System.out.println(time++/60 + " min. " + time%60 + " sek.");
}
}
};
new Thread(runner).start();
String s, out = "";
while ((s = JOptionPane.showInputDialog("Wprowadź jakiś tekst:")) != null)
out += " " + s;
System.out.println(out);
System.exit(0);
}
public static void main(String args[]) {
new AnonymousRunnable();
}
}
Możliwe są różnorakie schematy użycia (lokalnych) anonimowych klas wewnętrznych.
Niektóre standardowe metody Javy mają jako argument referencję do obiekty
typu Runnable i argument ten możemy podawać jako referencję do obiektu anonimowej klasy wewnętrznej, zdefiniowanej
"w miejscu" podania argumentu.
4.3. Kończenie pracy wątku
Wątek kończy pracę w sposób naturalny wtedy, gdy zakończy się jego metoda run().
Jeśli chcemy programowo zakończyć pracę wątku, to należy zapewnić w metodzie
run() sprawdzenie warunków zakończenia (ustalanych programowo) i jeśli są
spełnione - spowodować wyjście z run() albo przez "dobiegnięcie do końca",
albo przez return.
Warunki zakończenia mogą być formułowane w postaci
wartości jakiejś zmiennej, które są ustalane przez inne fragmenty kodu programu
(wykonywane w innym wątku).
W klasie Thread znajduje się metoda stop(), która kiedyś służyła do kończenia
działania watków. Stwierdzono jednak, że jej użycie może powodować błędy
w programach i w tej chwili nie należy jej już używać.
W naszym samochodowym przykładzie klasa Vehicle (którą dziedziczy Car) dostarcza
metody getState(), która zwraca aktualny stan pojazdu (m.in. czy jedzie).
Możemy spróbować jej użyć do sprawdzenia warunku kontynuacji działania wątku
zużycia paliwa: jeżeli samochód jedzie, to paliwo jest zużywane, w przeciwnym
razie wątek zużycia paliwa powinien zakończyć działanie.
public void run() {
while(getState() == MOVING) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
fuel--;
System.out.println("Paliwo: " + fuel); // śledzimy ile jest paliwa
if (fuel <= 0) stop(); // jeżeli brak paliwa...zatrzymujemy samochód
}
}
Wynik jest w miarę satysfakcjonujący:
Paliwo: 9
Paliwo: 8
Paliwo: 7
Samochód nr rej WA1090 - STOI
Paliwo: 6
Samochód nr rej WA1090 - STOI
Oto wydruk testu, w którym
- tak jak w końcu poprzedniego podpunktu - po trzech sekundach zatrzymujemy
samochód. Okazuje się, że samochód zużył 4 litry paliwa, choć symulowany
czas jazdy wynosił ok. 3 sekund. W innym przebiegu programu mogłoby się
okazać, że zużyto trzy (a nie cztery) litry paliwa. O tym dlaczego tak jest
- dowiemy się dalej.
Zwróćmy teraz uwagę na inny problem, który występuje w naszym programie
samochodowym.. Po wprowadzeniu kodu dla wątku zużycia paliwa nasza klasa
Car nie jest odporna na błąd dwukrotnego uruchomienia samochodu (wydania
samochodowi polecenia start dwa razy).
Owszem, w klasie Vehicle zagwarantowaliśmy
brak możliwości przejścia ze stanu MOVING do MOVING (czyli bronimy się przed
błędem, i wypisywany jest komunikat "Nie jest możliwe przejście ze stanu
JEDZIE do stanu JEDZIE"), ale tworzenie wątku zużycia paliwa w metodzie start()
klasy Car nie uwzględnia tego. Napisaliśmy tam po prostu:
new Thread(this).start();
i za każdym razem, kiedy wywołujemy metodę start na rzecz tego samego samochodu
tworzony jest i uruchamiany nowy wątek zużycia paliwa (o ile paliwo jeszcze jest).
Modyfikacja :
- konstruktor Thread(Runnable, String), który ma dodatkowy argument
typu String, dzięki czemu każdemu wątkowi możemy nadać unikalną nazwę,
- statyczną metodę currentThread() z klasy Thread, która zwraca referencję do bieżącego wątku (tego, ktory wykonuje ten fragment kodu, w którym znajduje się wywołanie tej metody).
- metodę getName z klasy Thread, która zwraca nazwę watku (w szczególnoście tę nadaną za pomocą konstruktora).
class Car extends Vehicle implements Runnable {
...
int tnr = 1; // numer wątku
// Start samochodu
public void start() {
if (fuel > 0) {
super.start();
new Thread(this, "Nr " + tnr++).start();
}
else System.out.println("Brak paliwa");
}
...
public void run() {
Thread cThread = Thread.currentThread();
while(getState() == MOVING ) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
fuel--;
System.out.println("Paliwo zużywa wątek: " + cThread.getName() +
", pozostało paliwa: " + fuel);
if (fuel <= 0) stop();
}
}
}
class Test {
....
public static void main(String[] args) {
Car c = new Car(...);
c.fill(10);
c.start();
c.start();
c.start();
delay(10);
...
}
}
Wynik działania programu pokazuje, że zostały uruchomione trzy wątki zużycia
paliwa, co powoduje trzykrotnie szybsze jego wyczerpanie:
Nie jest mozliwe przejscie ze stanu JEDZIE do stanu JEDZIE // to są komunikaty z klasy Vehicle
Nie jest mozliwe przejscie ze stanu JEDZIE do stanu JEDZIE
Paliwo zużywa wątek: Nr 2, pozostało paliwa: 9
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 8
Paliwo zużywa wątek: Nr 3, pozostało paliwa: 7
Paliwo zużywa wątek: Nr 2, pozostało paliwa: 6
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 5
Paliwo zużywa wątek: Nr 3, pozostało paliwa: 4
Paliwo zużywa wątek: Nr 2, pozostało paliwa: 3
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 2
Paliwo zużywa wątek: Nr 3, pozostało paliwa: 1
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 0
Niewątpliwie musimy zabezpieczyć się przed wielokrotnym uruchamianiem wątków
zużycia paliwa dla tego samego samochodu.
Postaramy sie zatem bezpośrednio w klasie Car wprowadzić zmienną, która będzie
kontrolować działanie wątku zużycia paliwa. Wygodna okaże się tu zmienna,
która reprezentuje sam wątek zużycia paliwa. Jej wykorzystanie pozwala na
proste tworzenie i uruchamianie wątku (wtedy, gdy wątek jeszcze nie istnieje),
a także łatwe kończenie pracy wątku.
Jest to metoda bardzo często stosowana w programach, gdzie potrzebne jest
wielokrotne uruchamianie i kończenie jakiegoś watku, a jednocześnie trzeba
zagwarantować, by w każdym momencie działał tylko jeden wątek, wykonujący
kod podanej metody run().
Wprowadzimy zatem do klasy Car definicję zmiennej fuelConsumeThread, która
będzie referencją do wątku zużycia paliwa. Inicjalnie będzie ona miała wartość
null. Każde wywolanie metody start() wobec samochodu będzie sprawdzać, czy
zmienna fuelConsumeThread ma wartośc null. Jeśli tak, to wątek nie istnieje
i trzeba go utworzyć i uruchomić. Jeśli nie - wątek już działa i nie należy
nic robić.
W metodzie run() możemy pobrać referencję do bieżącego wątku (tego, który
aktualnie wykonuje tę metodę run). Metoda run powinna zakończyć działanie,
kiedy referencja do bieżącego wątku nie będzie równa zapamiętanej w zmiennej
fuelConsumeThread referencji do wątku zużycia paliwa.
Jeśli więc zmiennej fuelConsumeThread nadamy (w wątku głównym programu) wartość null, to uzyskamy trzy efekty:
- wątek zużycia paliwa zakończy działanie,
- ten obiekt-wątek (który wlaśnie zakończył działanie) będzie usunięty z pamięci przez odśmiecacza,
- kolejne wywołanie metody start() wobec samochodu stworzy i uruchomi nowy wątek zużycia paliwa.
public class Car extends Vehicle implements Runnable {
...
private Thread fuelConsumeThread;
...
// Start samochodu
public void start() {
if (fuel > 0) {
super.start();
if (fuelConsumeThread == null) {
fuelConsumeThread = new Thread(this);
fuelConsumeThread.start();
}
}
else System.out.println("Brak benzyny");
}
// Zatrzymanie samochodu
public void stop() {
fuelConsumeThread = null;
super.stop();
}
// Zużycie paliwa - wykonuje się jako wątek
public void run() {
Thread cThread = Thread.currentThread();
while(cThread == fuelConsumeThread ) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
fuel--;
System.out.println("Pozostało paliwa: " + fuel);
if (fuel <= 0) stop();
}
}
....
}
Zwróćmy uwagę, że wątek główny (w którym wykonują się metody start
oraz stop, wywoływane na rzecz samochodu) odwołuje się (właśnie w tych metodach)
do zmiennej fuelConsumeThread. Do tej samej zmiennej odwołuje się wątek zużycia
paliwa. Dwa wątki równolegle operują na tym samym elemencie obiektu.
Wydawałaby się, że nie ma w tym nic zlego. Jednak ze względu na efektywność
działania, w Javie wątki operują na kopiach zmiennych, uzgadniając ich wartości
z oryginałami tylko w niektórych punktach wykonania programu, tzw. punktach
synchronizacji. Zatem metoda stop() lub start()
z klasy Car może zmienić wartość swojej kopii zmiennej fuelConsumeThread,
a metoda run() - wykonująca się w wątku zużycia paliwa będzie sprawdzać wartość
swojej kopii tej zmiennej i nie będzie dostatecznie wcześniej "widziała"
dokonanej zmiany.
Aby zabezpieczyć się przed tą konkretną sytuacją można zadeklarować współdzieloną zmienną ze specyfikatorem volatile
(co oznacza, że wartości oryginału i kopii zmiennej będą uzgadniane przy
każdym odwołaniu do niej) lub też zastosować mechanizmy synchronizacji (inaczej
zwane wykluczaniem), bo właśnie w punktach synchronizacji na pewno następuje
uzgadnainie kopii z oryginałem. Synchronizacja dzialania watków ma jednak
dużo szersze znaczenie i pora teraz na jej omówienie.
4.4. Synchronizacja wątków
Rozważmy następujący przykład.
Oto prosta klasa Balance, z jednym polem - liczbą całkowitą i metodą balance(),
która najpierw zwiększa wartość tej liczby, a następnie ją zmniejsza, po
czym zwraca wynik - wartość tej liczby.
class Balance {
private int number = 0;
public int balance() {
number++;
number--;
return number;
}
}
Wydaje się nie podlegać żadnej watpliwości, że jakiekolwiek wielokrotne wywoływanie
metody balance() na rzecz dowolnego obiektu klasy Balance zawsze zwróci wartość
0.
Otóż, w świecie programowania współbieżnego nie jest to wcale takie oczywiste!
Więcej: wynik różny od 0 może pojawiać się nader często!
Przekonajmy się o tym poprzez wielokrotne wywoływanie metody balance() na rzecz tego samego obiektu w kilku różnych wątkach.
Każdy z wątków będziemy tworzyć i uruchamiać poprzez stworzenie obiektu poniższej
klasy BalanceThread, dziedziczącej Thread, i wywołanie na jego rzecz metody
start(). Przy tworzeniu nazwiemy każdy z wątków (parametr name konstruktora).
Wielokrotne wywołania metody balance() zapiszemy w pętli w metodzie run().
Obiekt na rzecz którego jest wywoływana metoda oraz liczbę powtórzeń pętli
przekażemy jako dwa pozostałe argumenty konstruktora.
Tuż przed zakończeniem metody run() pokażemy jaki był wynik ostatniego odwołania do metody balance().
class BalanceThread extends Thread {
private Balance b; // referencja do obiektu klasy Balance
private int count; // liczba pwotórzeń pętli w metodzie run
public BalanceThread(String name, Balance b, int count) {
super(name);
this.b = b;
this.count = count;
start();
}
public void run() {
int wynik = 0;
// W pętli wielokrotnie wywołujemy metodę balance()
// na rzecz obiektu b klasy Balance.
// Jeżeli wynik metody jest różny od zera - przerywamy działanie pętli
for (int i = 0; i < count; i++) {
wynik = b.balance();
if (wynik != 0) break;
}
// Pokazujemy wartość zmiennej wynik na wyjściu z metody run()
System.out.println(Thread.currentThread().getName() +
" konczy z wynikiem " + wynik);
}
}
W klasie testującej stworzymy obiekt klasy Balance, po czym stworzymy i uruchomimy
podaną przez użytkownika liczbę wątków, które za pomocą metody run() z klasy
BalanceThread będą równolegle operować na tym obiekcie wielokrotnie wywołując
na jego rzecz metodę balance() z klasy Balance.
class BalanceTest {
public static void main(String[] args) {
int tnum = Integer.parseInt(args[0]); // liczba wątków
int count = Integer.parseInt(args[1]); // liczba powtórzeń pętli w run()
// Tworzymy obiekt klasy balance
Balance b = new Balance();
// Tworzymy i uruchamiamy wątki
Thread[] thread = new Thread[tnum]; // tablica wątków
for (int i = 0; i < tnum; i++)
thread[i] = new BalanceThread("W"+(i+1), b, count);
// czekaj na zakończenie wszystkich wątków
try {
for (int i = 0; i < tnum; i++) thread[i].join();
} catch (InterruptedException exc) {
System.exit(1);
}
System.out.println("Koniec programu");
}
}
Uwaga: metoda join z klasy Thread powoduje oczekiwanie na zakończenie
wątku, na rzecz któego została wywołana. Oczekiwanie może być przerwane,
gdy wątek został przerwany przez inny wątek - wtedy wystąpi wyjątek InterruptedException.
Uruchamiając aplikację z podanymi jako argumenty liczbą wątkow = 2 oraz liczbą
powtorzeń pętli w metodzie run() = 100000, nader często zyskamy intuicyjnie
oczekiwany wynik (W1 konczy z wynikiem 0, W2 konczy z wynikem 0). Może się
jednak zdarzyć wynik inny! Zwiększenie liczby wątków i liczby powtórzeń pętli
prawie na pewno pokaże nam, że niektóre wątki zakończą działanie z wynikem
różnym od 0.
Na przyklad, przy liczbie wątkow = 5 i liczbie powtórzeń pętli = 1000000, możemy raz uzyskac następujący wynik:
W2 konczy z wynikiem 0
W3 konczy z wynikiem 0
W4 konczy z wynikiem 0
W1 konczy z wynikiem 0
W5 konczy z wynikiem 0
a za chwilę, przy ponowym uruchomieniu z tymi samymi argumentami:
W1 konczy z wynikiem 1
W3 konczy z wynikiem 1
W2 konczy z wynikiem 1
W5 konczy z wynikiem 0
W4 konczy z wynikiem 0
Testowanie programów wielowątkowych jest trudne, bowiem możemy wiele razy
otrzymać wyniki, które wydają się świadczyć o poprawności programu, a przy
kolejnym uruchomieniu okaże się, że wynik jest nieprawidłowy. Wyniki uruchamiania
programów wielowątkowych mogą być także różne na różnych platformach systemowych.
Powstaje oczywiste pytanie: jak to się dzieje, że w powyższym przykładowym
programie uzyskujemy wyniki, których - wydaje się na podstawie analizy kodu
metody balance() - nie sposób uzyskać?
Otóż, wszystkie wykonujące (tę samą) metodę run() wątki odwołują się do tego
samego obiektu klasy Balance (w programie oznaczanego przez b). Mówimy: współdzielą
obiekt.
Obiekt ten ma jeden element - odpowiadający zmiennej number zdefiniowanej jako pole klasy Balance.
Wywolywana przez wątki na rzecz tego obiektu metoda balance() zwiększa a następnie zmniejsza wartość tej zmiennej.
Wyobraźmy sobie, że działają dwa wątki. Jeden z nich uzyskuje czas procesora
i rozpoczyna wykonanie metody balance(). Po zwiększeniu o 1 zmiennej number
zostaje wywlaszczony (zmienna number ma teraz wartość 1). Czas procesora
przydzielany jest drugiemu wątkowi. Drugi wątek rozpoczyna wykonanie metody
balance() i zwiększa o 1 wartość zmiennej number (zmienna number ma teraz
wartość 2), po czym zostaje wywlaszczony, a czas procesora przydzielony zostaje
wątkowi pierwszemu, który kontynuuje wykonanie metody run() od miejsca, w
którym odebrano mu czas procesora. Zmniejsza on teraz zmienną number i zwraca
wynik, który równy jest 1. Po zakończeniu wątku pierwszego, pracę kontynuuje
wątek drugi. Po zmniejszeniu zmiennej number zwraca wynik 0.
Obrazuje to poniższy rysunek.
Komentarze do rysunku:
- Punkt a - wątek W1, po zwiększeniu zmiennej number zostaje wywłaszczony. Zaczyna działanie wątek W2
- Punkt b - wątek W2 po zwiększeniu zmiennej number zostaje wywłaszony.
Pracę kontunuuje wątek W1 (mając za wartość zmiennej number 2)
- Punkt c - po zakończeniu wątku W1 (który zmniejszył number i zwrócił
1), wątek W2 kontunnuje pracę: zmniejsza number i zwraca wynik 0.
Jest to sytuacja, która może się zdarzyć, ale nie musi. Wszystko zależy od
tego czy i kiedy (w jakim momecie wykonania metody run()) systemowy zarządca
wątków wywlaszczy wykonujący się aktualnie wątek. A z tym jest bardzo różnie
w zależności od platformy systemowej czy aktualnego obciążenia procesora.
Zawsze jednak musimy się liczyć z tym, że wątki operujące na wspóldzielonych
zmiennych mogą być wywłaszczone w trakcie operacji (nawet pojedyńczej) i
wobec tego stan wspóldzielonej zmiennej może okazać się niespójny.
Aby uniknąć równoczesnego działania wątków na tym samym obiekcie (co w sposób
nieprzewidywalny ukształtować może jego stany) stosuje się rygle.
Każdy egzamplarz klasy Object i jej podklas posiada rygiel.
Ryglowanie obiektów dokonuje się automatycznie i sterowane jest słowem kluczowym synchronized.
Po części ze względu na to slowo kluczowe mówimy o synchronizowaniu fragmentów kodu, wykonywanych przez wątki.
Synchronizowane mogą być metody i bloki.
Metoda synchronizowana oznaczana jest w deklaracji słowem synchronized:
synchronized void metoda() {
...
}
UWAGA: słowo kluczowe synchronized nie jest częścią sygnatury metody!
Zatem przedefiniowanie metody synchronizowanej może być synchronizowane albo nie
Kiedy dany wątek wywołuje na rzecz jakiegoś obiektu metodę synchronizowaną, automatycznie zamykany jest rygiel. Mówimy też: obiekt jest zajmowany przez wątek.
Inne wątki usiłujące wywołać na rzecz tego obiektu metodę synchronizowaną
(niekoniecznie tę samą, ale koniecznie synchronizowaną) lub też wykonać instrukcję synchronized z podanym odniesieniem do zaryglowanego obiektu (o tej instrukcji za chwilę) są blokowane i czekają
na zakończenie wykonania metody przez wątek, który zajął obiekt (zamknął rygiel).
Dowolne zakończenie metody synchronizowanej (również na skutek powstania wyjątku) zwalnia rygiel, dając czekającym wątkom możność dostępu do obiektu.
W naszym przykładowym programie (z klasami Balance i BalanceThread) dla zapewnienia
prawidłowego działania (otrzymywania z metody balance() wyników zawsze równych
0) można zatem zdefiniować metodę balance() w klasie Balance jako synchronizowaną.
class Balance {
private int number = 0;
synchronized public int balance() {
number++;
number--;
return number;
}
}
Wtedy każde wywołanie metody balance() przez jakiś wątek na rzecz obiektu,
oznaczanego (w klasie testującej) przez b spowoduje zajęcie obiektu (zamknięcie
rygla). Inne wątki starające się równolegle działać na tym samym obiekcie
za pomoca metody balance() będą czekać na zakończenie jej wykonania przez
wątek, który zajął obiekt. Zatem każdy wątek wykona obie operacje ++ i --
bez ingerencji ze strony innych wątków i wynik metody balance() zawsze będzie
równy 0.
Synchronizowanie wątków jest czasowo kosztowne. Łatwo się o tym przekonać
porównując czasy wykonania omawianego programu testującego z i bez synchronizacji
metody balance().
Ta czasochłonnośc wynika z operacji zamykania i otwierania rygli (które to operacje nie
tylko polegają na ustawianiu jakichś flag, ale wiążą się z prowadzeniem i
zmianami kolejek oczekującyh na zaryglowanym obiekcie watków). Gdy - w naszym
przykładzie - metoda balance() jest synchronizowana, to operacje te - w każdym
wątku - wykonywane są tyle razy ile wynosi liczba powtórzeń pętli w metodzie
run(). Przy dużej liczbie powtórzeń pętli nasz program może działać bardzo
długo.
Pewne zaawansowane sposoby strukturyzacji programów wielowątkowych pozwalają
w ogóle unikać synchronizacji przy jednoczesnym wykluczaniu możliwości powstawania
niespójnych stanów obiektów.
Należą do nich m.in.:
- użycie klas niezmiennych (immutables), bowiem obiekty niezmienne (właśnie
przez swą niezmienność) nie stwarzają problemu spójności stanów przy dostępie
z wielu wątków,
- użycie kodów wielobieżnych (reentrant); kody wielobieżne
są bezpieczne w użyciu wielowątkowym; przykładowo, kod który używa wyłącznie
zmiennych lokalnych i nie odnosi się do pól klasy jest wielobieżny i nie
wymaga synchronizacji,
- czasami można użyć zmiennych ze specyfikatorem volatile
(o którym była już mowa); generalnie dostęp do takich zmiennych nie musi
być synchronizowany dla operacji atomistycznych (wykonywanych "w jednym takcie"),
ale to czy dana operacja jest atomistyczna zależy od architektury systemowej
i implementacji JVM.
Bloki synchronizowane.
Bloki synchronizowane wprowadzane są instrukcją synchronized z podaną w nawiasie referencją do obiektu, który ma być zaryglowany.
synchronized (lock) {
// ... kod
}
gdzie: lock - referencja do ryglowanego obiektu
kod - kod bloku synchronizowanego
Wykonanie instrukcji synchronized przez wątek rygluje obiekt, do którego referencja podana jest w nawiasach tej instrukcji.
Inne wątki, które usiłują operowac na tym obiekcie za pomocą metod synchronizowanych
lub wykonać instrukcję synchronized z referencją do tego obiektu są blokowane
do chwili gdy wykonanie kodu bloku synchronizowanego nie zostanie zakończone
przez wątek zajmujący obiekt.
Bloki synchronizowane są ogólniejsze od metod synchronizowanych, bowiem zapis każdej synchronizowanej metody:
synchronized void metoda() {
// kod
}
jest równoważny zapisowi:
void metoda() {
synchronized(this) {
// kod
}
}
natomiast za pomocą instrukcji synchronized możemy również synchronizować dowolne fragmenty kodu wewnątrz metod.
W naszym przykładzie "bilansowym" synchronizację wątków możemy uzyskać używając
instrukcji synchronized w metodzie run() klasy BalanceThread.
class BalanceThread extends Thread {
private Balance b; // referencja do obiektu klasy Balance
private int count; // liczba powtórzeń pętli w metodzie run
public BalanceThread(String name, Balance b, int count) {
super(name);
this.b = b;
this.count = count;
start();
}
public void run() {
int wynik = 0;
synchronized(b) {
for (int i = 0; i < count; i++) {
wynik = b.balance();
if (wynik != 0) break;
}
System.out.println(Thread.currentThread().getName() +
" konczy z wynikiem " + wynik);
}
}
}
Łatwo się przekonać, że to rozwiązanie (w tym konkretnym przypadku) jest
lepsze, bowiem ryglowanie obiektu b odbywa się tylko tyle razy ile jest wątków,
a nie przy każdym powtórzeniu pętli for.
class SynchroConstr {
Object lock = new Object(); // semafor
SynchroConstr() {
synchronized(lock) {
// ... kod konstruktora
}
}
}
Alternatywnie można użyć synchronizacji na obiekcie-klasie (obiekcie klasy Class, który reprezentuje daną klasę) np.
synchronized(SynchroConstr.class) {
...
}
Uwaga: literał nazwa_klasy.class oznacza obiekt-klasę nazwa_klasy.
Na takich obiektach-klasach są zresztą synchronizowane metody statyczne:
class Klasa {
public static synchronized void jakasMetoda() { .. }
}
jest równoważne:
class Klasa {
public static void jakasMetoda() {
synchronized(Klasa.class) {
// ... kod metody
}
}
}
Warto podkreślić, że zawsze powinniśmy mieć pełną świadomość jaki obiekt jest ryglowany. Rozważmy przykład.
class Liczba {
static double n;
synchronized static void set(double x) { n = x; } // 1
synchronized double get() { return x; } // 2
}
Tutaj metoda set jest synchronizowana na obiekcie Liczba.class (obiekcie-klasie),
a metoda get - na obiekcie this (obiekcie klasy Liczba, na rzecz którego
jest wołana). To są dwa różne obiekty, dwa różne rygle. Zatem nie uzyskujemy
wykluczania w dostępie do pola n!
W takich przypadkach powinniśmy użyć synchronizacji na obiekcie Liczba.class
(wprowadzając do metody get blok synchronizowany), albo realizować dostęp
do pól statycznych wyłącznie za pomocą statycznych metod synchronizowanych.
Podobna sytuacja dotyczy metod definiowanych w klasach wewnętrznych np.
class Outer {
double n;
class Inner {
synchronized void metoda() {
// ... dostęp do pola n klasy otaczającej
}
}
}
nie synchronizuje dostępu do pola n (bo mamy tu synchronizację na obiekcie klasy wewnętrznej, a nie otaczającej).
W tym przypadku należy napisać tak:
class Inner {
void metoda() {
synchronized(Outer.this) {
// dostęp do pola n klasy otaczającej
}
}
Na koniec warto wspomnieć o problemie synchronizacji przy korzystaniu z singletonów.
Generalnie, powinniśmy metodę statyczną getInstance() w klasie singletonu
(zwracającą jedyny obiekt klasy) uczynić synchronizowaną, bowiem jeśli dwa
wątki będą równolegle wykonywać tę metodę, to może się zdarzyć, że otrzymamy
dwa obiekty klasy (co przeczy definicji singletonu). Faktycznie, w poniższym kodzie:
class KlasaSingletonu {
private static KlasaSingletonu obj;
private KlasaSingletonu() {
// ...
}
public static KlasaSingletonu getInstance() {
if (obj == null) obj = new KlasaSingletonu();
return obj;
}
}
jeden z wątków może zostać wywłaszczony zaraz po sprawdzania warunku (obj
== null), przychodzący na jego miejsce drugi wątek może stworzyć obiekt,
a przywrócony pierwszy - "pamiętając", że obiektu nie było - też go stworzy,
Powinniśmy więc napisać tak:
public static synchronized KlasaSingletonu getInstance() {
if (obj == null) obj = new KlasaSingletonu();
return obj;
}
Warto zauważyć jednak, że taka synchronizacja jest potrzebna tylko przy pierwszym
wywołaniu metody getInstance(). Wszystkie inne wywołania będą miały charakter
odczytu, nie muszą zatem być synchronizowane, a ponieważ synchronizacja jest
kosztowna - to chcielibyśmy jej uniknąć.
Niestety - z tych samych powodów co poprzednio - poniższe rozwiązanie, wydawałoby
się logicznie synchronizowane tylko na pierwszym odwolaniu i unikające synchronizacji
przy następnych - będzie wadliwe:
public static KlasaSingletonu getInstance() {
if (obj == null) { // w tym if (...) mamy ten sam problem co poprzednio
synchronized(KlasaSingletonu.class) {
obj = new KlasaSingletonu();
}
}
return obj;
}
Aby uniknąć synchronizowanych odwołań powinniśmy zatem stworzyć obiekt przy
pierwszym odwołaniu do klasy, co można uzyskać np. tak:
class KlasaSingletonu {
private static final KlasaSingletonu obj = new KlasaSingletonu();
private KlasaSingletonu() { // prywatny konstruktor
// ...
}
public static KlasaSingletonu getInstance() {
return obj;
}
}
Podsumowanie synchronizacji:
Synchronizacja jest mechanizmem, który zapewnia, że kilka wykonujących się watków:
- nie będzie równocześnie działać na tym samym obiekcie,
- nie będzie równocześnie wykonywać tego samego kodu.
Synchronizację wątków uzyskuje się za pomocą obiektów, które wykluczają równoczesny
dostęp wątków do zasobów i/lub równoczesne wykonanie przez wątki tego samego
kodu. Takie obiekty nazywają się ogólnie muteksami (od ang. mutual-exclusion semaphore). W Javie rolę muteksów pełnią rygle (ang lock).
O ryglowaniu (wprowadzanym za pomocą słowa kluczowego synchronized) możemy
myśleć jako o zapewnieniu wyłącznego dostępu do pól obiektu lub (statycznych)
pól klasy, ale równie dobrze "zaryglowany" obiekt może spelniać rolę muteksu,
zabezpieczającego fragment kodu przed równoczesnym wykonaniem przez dwa wątki.
Kod, który może być wykonywany w danym momencie tylko przez jeden wątek nazywa się sekcją krytyczną.
W Javie sekcje krytyczne wprowadza się jako bloki lub metody synchronizowane.
Użycie sekcji krytycznych pozwala na prawidłowe współdzielenie zasobów przez wątki.
W polskiej literaturze przedmiotu używa się także terminów:
- zajmowanie zasobu (obiektu) przez wątek,
- wzajemne wykluczanie wątków w dostępie do zasobu (obiektu).
Pojęcia te można traktowac jako szczególne przypadki synchronizacji, a ponieważ
prawidłowe współdzielenie zasobów jest w programowaniu współbieżnym kluczowe,
to często utożsamiamy je z synchronizacją.
Nie należy jednak sądzić, że synchronizacja wątków oznacza zagwarantowanie
określonej, konkretnej kolejności dostępu wątków do wspóldzielonych zasobów.
Ustalanie i kontrolowanie konkretnej kolejności dostępu wątkow (często zależnej
od wyników wytwarzanych przez wykonywane przez nie kody) do wspóldzielonych
zasobów będziemy nazywać koordynacją wątków.
4.5. Koordynacja wątków
Ryglowanie (użycie synchronized) służy do zapobiegania niepożądanej interakcji wątków.
Nie jest ono jednak wystarczającym środkiem dla zapewnienia współdziałania wątków.
Przykład:
Dwa wątki Author i Writer mogą odwoływać się do tego samego obiekty typu Teksty.
Author podrzuca teksty, zapisywane w polu txt, Writer wypisuje je na konsoli.
Do ustalania tekstów służy metoda setTextToWrite (wywołuje ją Author), teksty
do zapisu odczytywane są przez Writera za pomocą metody getTextToWrite i
wypisywane na konsoli.
Ponieważ metody te mogą być wywołane równocześnie (z różnych wątków) i operują
na polu tego samego obiektu, winny być synchronizowane.
Ale tu ważna jest również kolejność i koordynacja działań obu wątków.
Chodzi o to, by Writer zapisywał tylko raz to co poda Autor, a Autor nie
podawał nic nowego, dopóki Writer nie zapisze poprzednio podanego tekstu.
Skoordynowanie interakcji pomiędzy wątkami uzyskuje się za pomocą metod klasy Object:
Koordynacja działań wątków sprowadza się do następujących kroków:
- Wątek wywołuje metodę wait na rzecz danego obiektu, gdy oczekuje,
że ma się coś (zwykle w kontekście tego obiektu) zdarzyć (zwykle jest to
pewna oczekiwana zmiana stanu obiektu, której ma dokonać inny wątek i która
jest realizowana np. przez zmianę wartości jakiejś zmiennej - pola obiektu).
- Wywołanie wait blokuje wątek (jest on odsuwany od procesora), a jednocześnie powoduje otwarcie rygla
zajętego przez niego obiektu, umożliwiające dostęp do obiektu z sekcji krytcznych innych wątków
(wait może być wywołane tylko z sekcji krytycznej, bowiem chodzi tu
o współdziałanie wątków na tym samym ryglowanym obiekcie, a zatem konieczna
jest synchronizacja). Inny wątek może teraz zmienić stan obiektu i powiadomić
o tym wątek czekający (za pomocą metody notify lub notifyAll).
- Odblokowanie (przywrócenie gotowości działania i ew. wznowienie działania wątku) następuje, gdy inny wątek wywoła metodę notify lub notifyAll na rzecz tego samego obiektu, "na którym" dany wątek czeka (na rzecz ktorego wywołał metodę wait).
- Metoda notifyAll odblokowuje wszystkie czekające na danym obiekcie wątki
- Wywołanie notify lub notifyAll musi być także zawarte w sekcji krytcznej.
Działanie metod wait, notify, notifyAll zawsze są związane z konkretnymi
obiektami, a jednocześnie dotyczy wątków (czyli zazwyczaj innych obiektów),
które na tych konkretnych obiektach operują
.
Spójrzmy na przykład (schemat) prawidłowej koordynacji:
class X {
int n;
boolean ready = false;
....
synchronized int get() {
try {
while(!ready)
wait();
} catch (InterruptedException exc) { .. }
ready = false;
return n;
}
synchronized void put(int i) {
n = i;
ready = true;
notify();
}
}
Uwaga: metoda wait() może sygnalizować wyjątek InterruptedException (w
przypadku, gdy nastąpiło zewnętrzne przerwanie oczekiwania na skutek użycia
w innym wątku metody interrupt()). Wyjątek ten musimy obsługiwać.
Wyobraźmy sobie, że działają tu dwa wątki - ustalający wartość n za pomocą put i pobierający wartość n za pomocą get.
Wątek pobierający musi czekać, aż wątek ustalający ustali wartość n (wait).
Ustalenie wartości powoduje dwie zmiany: warunek "wartość gotowa" staje się true, a oczekiwanie jest przerywane przez notify.
Zwrócmy uwagę, że metody wait i notify są wywoływane na rzecz tego obiektu,
na rzecz którego wywołano metody get i put (moglibyśmy napisać - dla większej
jasności: this.wait() i this.notify()).
Oczekiwanie kończy się naprawdę dopiero wtedy, gdy spełniony jest jakiś warunek.
UWAGA: warunek zakończenia oczekiwania należy sprawdzać w pętli. Nie ma bowiem
gwarancji, że po odblokowaniu wątku czekającego warunek nadal będzie spełniony.
Zgodną z przedstawionym schematem realizację omówionego wcześniej
przykładu Author-Writer pokazano na wydruku poniżej. W programie wątek-Autor
co jakiś czas (lgenerowany losowo) ustala tekst do napisania (sa to kolejne
elmenty tablicy napisów). Wątek-Writer pobiera ustalony tekst i wypisuje
na konsoli. Zakończenie pracy Autor sygnalizuje poprzez podanie tekstu =
null.
// Klasa dla ustalania i pobierania tekstów
class Teksty {
String txt = null;
boolean newTxt = false;
// Metoda ustalająca tekst - wywołuje Autor
synchronized void setTextToWrite(String s) {
while (newTxt == true) {
try {
wait();
} catch(InterruptedException exc) {}
}
txt = s;
newTxt = true;
notifyAll();
}
// Metoda pobrania tekstu - wywołuje Writer
synchronized String getTextToWrite() {
while (newTxt == false) {
try {
wait();
} catch(InterruptedException exc) {}
}
newTxt = false;
notifyAll();
return txt;
}
}
// Klasa "wypisywacza"
class Writer extends Thread {
Teksty txtArea;
Writer(Teksty t) {
txtArea=t;
}
public void run() {
String txt = txtArea.getTextToWrite();
while(txt != null) {
System.out.println("-> " + txt);
txt = txtArea.getTextToWrite();
}
}
}
// Klasa autora
class Author extends Thread {
Teksty txtArea;
Author(Teksty t) {
txtArea=t;
}
public void run() {
String[] s = { "Pies", "Kot", "Zebra", "Lew", "Owca", "Słoń", null };
for (int i=0; i<s.length; i++) {
try {
sleep((int)(Math.random() * 1000));
} catch(InterruptedException exc) { }
txtArea.setTextToWrite(s[i]);
}
}
}
// Klasa testująca
public class Koord {
public static void main(String[] args) {
Teksty t = new Teksty();
Thread t1 = new Author(t);
Thread t2 = new Writer(t);
t1.start();
t2.start();
}
}
Pojęcie monitora
Każdy obiekt oprócz rygla posiada "kolejkę oczekiwania" (wait set). Ogólnie
kolejka ta zawiera odniesienia do wszystkich wątków zablokowanych na obiekcie
(metodą wait) i czekających na powiadomienie o możliwości wznowienia działania.
Kolejka oczekiwania jest "prowadzona" przez JVM, a jej zmiany mogą być uzyskane
tylko metodami wait, notify, notifyAll (z klasy Object) oraz interrupt (z
klasy Thread).
Twory, które mają rygle i kolejki oczekiwania nazywane są monitorami.
Generalnie - monitor jest fragmentem kodu programu (niekoniecznie ciągłym),
do którego dostęp zabezpieczany jest przez rygiel (muteks). W odróżnienuiu
od sekcji krytycznych - monitory są powiązane z obiektami, ich stanami. Dlatego
mówimy czasem krótko: "obiekt ma monitor", "monitor obiektu" lub nawet "obiekt jest monitorem".
4.6. Stany wątków
Wątek może znajdować się w czterech stanach: New Thread, Runnable, NotRunnable, Dead.
Stan New Thread powstaje w momecie stworzenia obiektu-wątku.
Do stanu Runnable wątek przechodzi po wywołaniu metody start(), która z kolei wywołuje run().
Stan Runnable niekoniecznie oznacza, że wątek jest aktualnie wykonywany.
Raczej jest to potencjalna gotowość do działania, a to czy system operacyjny
akurat przydzieli temu wątkowi czas procesora zależy od wielu czynników (jaki
jeszcze inne wątki konkurują o czas procesora, jaki jest schemat zarządzania
wątkami, jakie są ich priorytety itp.).
Do stanu NotRunnable wątek przechodzi na skutek:
- wywołania metody sleep (usypiającej wątek). Powrót do stanu Runnable - po upływie podanego czasu,
- albo wywołania metody wait (wątek czeka na powiadomienie o jakimś zdarzeniu).
Powrót do stanu Runnable po tym jak inny obiekt powiadamia wątek o wystąpieniu
zdarzenia za pomocą metody notify lub notifyAll,
- albo blokowania na operacjach wejścia-wyjścia. Powrót do stanu Runnable: po zakończeniu operacji,
- albo blokowanie na obiekcie z zamkniętym ryglem.
Wątek kończy działanie na skutek zakończenia metody run() i przechodzi do stanu Dead.
Mimo, iż nadal może istnieć obiekt oznaczający wątek, ponowne użycie wobec
niego metody start() jest niedozwolone (w Javie wątki nie są "restartowalne").
Aby ponownie uruchomić wątek, należy stworzyć nowy obiekt i zastosować metodę start().
4.7. Wstrzymywanie i przerywanie wątków
Działanie wątku może zostać wstrzymane i - później - wznowione. Niegdyś
służyły temu metody suspend() i resume() z klasy Thread.
Okazało się, że są one niespójne i mogą powodować zakleszczanie wątków.
Obecnie do wstrzymywania i przywracania działania wątków proponuje się mechanizm wait-notify.
Można użyć następującego prostego schematu.
Przykładowy schemat wstrzymywania i wznawiania wątków
|
Wstrzymaniem wątku dyryguje zmienna typu boolean o nazwie np. suspended
W metodzie run() zapisujemy:
while (warunek_działania_wątku) {
try {
synchronized(this) {
while (suspended) // warunek wstrzymania wątku
wait();
}
} catch(InterruptedException exc) { return; }
// ... praca wątku.
}
Inny wątek - gdy chce wstrzymać działanie tego wątku - ustala suspended = true.
Wznowienie osiągane jest przez suspended = false i notify() na rzecz tego wątku.
|
Użycie metody interrupt() z klasy Thread powoduje ustalenie statusu
wątku jako "przerwany" i - w niektórych sytuacjach - wygenerowanie wyjątku InterruptedException.
Nie należy mylić użycia metody interrupt() - opisywanej zresztą zawsze nieco
dwuznacznie jako metody "przerywającej wątek" - z zakończeniem działania
wątku.
Ogólnie, metoda ta ustala jedynie status wątku jako przerwany.
Kiedy wątek ze statusem "przerwany" jest (lub zostanie) zablokowany na wywołaniu metod:
jego status zmieniany jest na "nieprzerwany" i generowany jest wyjątek InterruptedException.
Jak juz widzieliśmy, przy wywołaniu tych metod wyjątek ten musimy obsługiwać.
W obsłudze wyjątku możemy zdecydować czy wątek ma być zakończony czy nie.
Przykładowy program pokazuje w jaki sposób możemy wstrzymywać, wznawiać,
kończyć i przerywać pracę wątku. Działania te próbujemy na wątku, który wypisuje
ciąg liczb rzeczywistych (od zera, zwiększanych co 1). Zwróćmy uwagę na użycie
metody isAlive() (zwraca true, jesli wątek nie jest w stanie Dead) i isInterrupted()
(zwraca true jeśli wątek ma status "przerwany"). Są to jedyne sposoby uzyskania
informacji o stanie wątku.
Warto też przetestować program i zobaczyć jakie są konsekwencje różnych poleceń
wydawanych przez użytkownika z dialogów wejściowych.
import javax.swing.*;
class SomeThread extends Thread {
volatile boolean stopped = false;
volatile boolean suspended = false;
public void run() {
double d = 0;
while(!stopped) {
try {
synchronized(this) {
while (suspended) wait();
}
} catch (InterruptedException exc) {
System.out.println("Obsluga przerwania watku w stanie wait");
}
if (suspended) System.out.println("Watek wstrzymany na wartosci " + d);
else System.out.println(++d);
}
}
public void stopThread() {
stopped = true;
}
public void suspendThread() {
suspended = true;
}
public void resumeThread() {
suspended = false;
synchronized(this) {
notify();
}
}
}
class ActionsOnThread {
public static void main(String args[]) {
String msg = "I = interrupt\n" +
"E = end\n" +
"S = suspend\n" +
"R = resume\n" +
"N = new start";
SomeThread t = new SomeThread();
t.start();
String cmd;
while ((cmd = JOptionPane.showInputDialog(msg)) != null) {
char c = cmd.charAt(0);
switch (c) {
case 'I' : t.interrupt(); break;
case 'E' : t.stopThread(); break;
case 'S' : t.suspendThread(); break;
case 'R' : t.resumeThread(); break;
case 'N' : if (t.isAlive())
JOptionPane.showMessageDialog(null, "Thread alive!!!");
else {
t = new SomeThread();
t.start();
}
break;
default : break;
}
JOptionPane.showMessageDialog(null,
"Command " + cmd + " executed.\n" +
"Thread alive ? " + (t.isAlive() ? "Y\n" : "N\n") +
"Thread interrupted ? " + (t.isInterrupted() ? "Y\n" : "N")
);
}
System.exit(0);
}
}
Kompozycja metod wait i interrupt, lub sleep i interrupt może służyć do kończenia pracy wątków.
Obrazuje to poniższy przykład, w którym wątek Increaser zwiększa licznik
Counter co freq milisekund, uzyskując efekt powtórzeń za pomocą metody wait(..),
która wstrzymuje działanie wątku na podany czas.
import javax.swing.*;
class Counter {
private double val;
public void incr() { val++; }
public String toString() { return String.valueOf(val); }
}
class Increaser extends Thread {
private Counter count; // zwiększany licznik
private int freq; // częstotliwość zwiększania
Increaser(Counter c, int f) {
super("Increasing thread");
freq = f;
count = c;
start();
}
public void run() {
while (true) {
synchronized(count) {
try {
count.wait(freq); // czeka freq ms
} catch (InterruptedException exc) {
System.out.println(getName() + " interrupted.");
return;
}
count.incr(); // zwiększenie licznika
System.out.println(count); // pokaz wartość licznika
}
}
}
}
class WaiterTest {
public static void main(String[] args) {
Counter counter = new Counter();
Increaser inc = new Increaser(counter, 1000);
int rc = JOptionPane.showConfirmDialog(null, "Czy zakończyć wątek?");
// Zakończenie pracy wątku za pomocą netody interrupt
if (rc == 0) inc.interrupt();
System.exit(0);
}
Tutaj niepotrzebne jest notify dla wznowienia wątku, bo dochodzi do niego
automatycznie po upływie czasu podanego w metodzie wait. W każdym momencie,
z zewnątrz, można za pomocą metody interrupt ustawić status wątku Increaser
jako przerwany. Gdy wątek jest zablokowany (wait jeszcze nie wróciło), to
oczekiwanie jest przerywane i generowany jest wyjątek InterruptedException
. Gdy natomiast w momencie wykonania interrupt wątek się wykonywał, to wyjątek
zostanie zgłoszony natychmiast po kolejnym wejściu w wait. W obsłudze wyjątku
kończymy wykonanie metody run. Pozwala to kończyć wątki w sposób niezależny
od częstotkiwości wykonania (wynikającej z czasu podanego w wait) czyli praktycznie
natychmiast, jesli tylko instrukcje pomiędzy kolejnymi wait są wykonywane
bardzo szybko.
Ten sam efekt możemy uzyskać, używając - np. w metodzie run() klasy Increaser - zamiast wait(freq) - sleep(freq).
Należy jednak pamiętać, że wait(...) zwalnia rygiel obiektu (udostępniając
go innym wątkom synchronizowanym na tym obiekcie), a sleep - nie.
Działanie wait i sleep możemy porównać modtfikując metodę main z poprzednidgo przykładu i uruchamiając
program w dwóch wersjach: z wait w metodzie run klasyIncreaser oraz zastępując
wait w tej metodzie - wywołaniem metody sleep.
public static void main(String[] args) {
int freq = 3000;
Counter counter = new Counter();
Increaser inc = new Increaser(counter, freq);
try { Thread.sleep(freq/3); } catch (Exception exc) { }
long startTime = System.currentTimeMillis();
synchronized(counter) {
counter.incr(); // próba zwiększenia licznika z tego wątku
}
System.out.println("Licznik " + counter + // jaki wynik? ile to trwało?
"\nCzas spędzony na counter.incr() " +
(System.currentTimeMillis() - startTime));
}
|
Przy użyciu metody wait() w run() klasy Increaser otrzymamy wydruk:
| Przy użyciu metody sleep() w run() klasy Increaser otrzymamy wydruk:
|
Licznik 1.0
Czas spędzony na counter.incr() 0
2.0
3.0
4.0
| 1.0
Licznik 2.0
Czas spędzony na counter.incr() 1970
3.0
4.0
|
Oba typy zachowań mogą być potrzebne w różnych sytuacjach. Gdy stosujemy
opisany mechanizm wyłącznie do oddawania czasu procesora i zapewnienia przy tym
możliwości zewnątrznego kończenia wątku, otwieranie rygla i udostępnianie
jakiegoś wspóldzielonego zasobu (obiektu) innym wątkom (czyli stosowanie
wait) może prowadzić do niespójności. Zatem, albo nalezy synchronizować wait na
obiekcie, który na pewno nie będzie wspóldzielony albo stosować sleep. Jednak
usypianie wątku za pomocą sleep w sekcji krytycznej może doprowadzić do zablokowania lub
bardzo niefektywnego działania programu (bowiem w trakcie uśpienia wątku
- inne wątki synchronizowane na obiekcie nie będą mogły się wykonywać). Trzeba
zatem dobierać w sleep odpowiednio małe takty czasowe.
Zobaczmy przykłąd zablokowania programu. Wprowadzimy tu, oprócz omawianej
klasy Increaser (z użyciem metody wait w run) - nową klasę Sleepy, która
reprezentuje wątek synchronizowany na tym samym obiekcie co "obliczeniowy"
kod metody run() w klasie Increaser. Wątek Sleepy w którymś momencie uśnie na długi
czas.
class Sleepy extends Thread {
private Counter count;
private final int N = 5;
public Sleepy(Counter c) {
count = c;
start();
}
public void run() {
for (int i=1; i <= N; i++) {
// w ostatniej iteracji usypiamy na długo
int time = (i == N ? 100000 : 1000);
synchronized(count) {
System.out.println("Sleep at " + count);
try {
sleep(time);
} catch (InterruptedException exc) {
}
}
}
}
}
Oba wątki Increaser i Sleepy są uruchamiane równolegle w metodzie main.
int freq = 3000;
Counter counter = new Counter();
Increaser inc = new Increaser(counter, freq);
new Sleepy(counter);
Wynik jest taki, że po upływie kilku sekund i wyprowadzeniu komunikatów:
Sleep at 0.0
Sleep at 0.0
Sleep at 0.0
1.0
Sleep at 1.0
Sleep at 1.0
program sprawia wrażenie zamrożonego, bowiem wątek Increaser jest zablokowany na zajętym przez "śpiocha" obiekcie counter.
Zauważmy, że za pomocą kombinacji wait-interrupt (bezpieczniejszej od sleep-interrupt)
można próbować tworzyć bardziej uniwersalne mechanizmy kończenia wątków.
Zobaczmy to na przykładzie klasy IterruptibleRepetitiveThread, która służy
do powtarzalnego wykonywania jakichś działań w wątku, przy jednoczesnym
zapewnienu możliwości kończenia pracy wątku za pomocą użycia na jego rzecz
metody interrupt. Działania do wykonania będą opisywane za pomocą implementacji
metody execute() interfejsu Command w konkretnych klasach. Przy tworzeniu
obiektów-wątków dostarczymy w konstruktorze obiekt klasy implementującej
interfejs Command a także częstotliwość z jaką ma być powtarzana metoda execute()
tej klasy. Po każdyym wykonaniu execute (a także w oczekiwaniu na kolejne
wykonanie) będzie można zakończyć działanie wątku.
// Implementacja tego interfejsu w jakiejś klasie
// dostarczy definicji metody do wykonania w wątku
interface Command {
public void execute();
}
// Klasa przerywalnych wątków
// wykonujących powtarzalne czynności
class InterruptibleRepetitiveThread extends Thread {
// obiekt dostarczający definicji czynności
private Command command;
// Muteks do synchronizacji wait
// Potrzebny jedynie dla zapewnienia reguł składniowych
// Obiekt ten na pewno nie będzie wspóldzielony!
private final Object waitMutex = new Object();
// Częstotliwość powtarzania czynności
int freq;
public InterruptibleRepetitiveThread(String name, int f, Command com) {
super(name);
command = com;
freq = f;
}
public void run() {
// Tu pętla nieskończona, można dodać warianty
// np. powtórzenie ileś razy
while (true) {
// Wykonanie czynności.
// Założenie: synchronizacja dostępu do jakichś wspóldzielonych
// obiektów - na poziomie definicji metody execute()!!!
command.execute();
// Umożliwienie zakończenia pracy wątku
synchronized(waitMutex) {
try {
waitMutex.wait(freq);
} catch (InterruptedException exc) {
System.out.println(getName() + " - interrupted");
return;
}
}
}
}
}
Klasę testująca działanie na przykładzie wątków współdzielących
obiekct liczniku pokazano na wydruku. Zwróćmy uwagę na konieczność synchromizacji
na poziomie definicji metod execute.
class Counter {
private double val;
public void increase() { val++; }
public String toString() { return String.valueOf(val); }
}
class InterruptibleTest {
Counter counter = new Counter(); // wspóldzielony licznik
InterruptibleTest() {
// Tworzymy i uruchamiamy piewrszy wątek
Thread licz1 = new InterruptibleRepetitiveThread("Licznik 1", 3000,
new Command() {
public void execute() { // konieczna synchronizacja
synchronized(counter) {
counter.increase();
System.out.println("Licznik 1 - " + counter);
}
}
});
licz1.start();
// Drugi wątek
Thread licz2 = new InterruptibleRepetitiveThread("Licznik 2", 3000,
new Command() {
public void execute() {
synchronized(counter) { // konieczna synchronizacja
counter.increase();
counter.increase();
counter.increase();
System.out.println("Licznik 2 - " + counter);
}
}
});
licz2.start();
// Dialog umożliwiający zakończenie wątków
String[] opcje = { "Licznik 1", "Licznik 2", "Oba liczniki" };
// Dopóki działa któryś z wątków
while (licz1.isAlive() || licz2.isAlive()) {
int rc = JOptionPane.showOptionDialog(null, "Który wątek zakończyć?",
null, 0, 0, null, opcje, opcje[2]);
switch (rc) {
case 0 : licz1.interrupt(); break;
case 1 : licz2.interrupt(); break;
case 2 : licz1.interrupt();
licz2.interrupt();
break;
default : break;
}
Thread.currentThread().yield(); // samowywłaszczenie
}
System.exit(0);
}
public static void main(String[] args) {
new InterruptibleTest();
}
}
Działanie programu ilustruje rysunek.
Warte podkreślenia w pokazanym przykładzie testowym są dwie kwestie.
Po piewrsze, trzeba zapewnić synchronizację w dostępie do obiektu counter,
ale zastosowanie synchronizacji metody execute (synchronized execute) nie
spełnia tego zadania, bo metoda ta jest definiowana w klasie wewnętrznej
i byłaby synchronizowana na jej obiekcie, a nie na obiekcie klasy Counter.
Po drugie, przed ponownym wyświetleniem dialogu należy dobrowolnie oddać
czas procesora, aby metody isAlive właściwie odczytały stan wątków (to, że
jakichś z nich został zakończony). Nie było sensu usypiać wątku głównego
(pokazującego dialogi), wystarczyło posłużyć się "lekką" odmianą samowywłaszczenia
- wywołaniem metody yield(). Nie przesuwa ona wątku do stanu NotRunnable
(jak sleep czy wait), ale powoduje oddanie czasu procesora (jednak tylko
jeśli czekające wątki mają równy lub wyższy priorytet od tego który zrzeka
się - na chwilę - dostępu do procesora).
4.8. Priorytety wątków, granulacja czasu i samowywłaszczanie
Przyjrzyjmy się jeszcze raz działaniu wątków, dodając do wcześniej pokazanej klasy Car
metodę getFuel(), zwracającą stan paliwa i testując klasę Car w poniższej
klasie.
public class TestCarE {
static int runTime;
static void delay(Car c, int sek) {
if (c.getState() == Car.MOVING) runTime += sek;
System.out.println(c + " przez " + sek + " sek....");
while(sek-- > 0) {
try {
Thread.sleep(1000);
} catch (Exception exc) { }
}
}
public static void main(String[] args) {
Car c = new Car("WA1090", new Person("Janek", "0909090"),
100, 100, 100, 100, 50);
c.fill(10); // napełniamy bak
c.start(); // ruszamy ...
delay(c, 3); // niech upłynie 3 sek. jazdy od tego momentu
c.stop(); // zatrzymujemy samochód
delay(c, 1); // czekamy sekundę...
c.start(); // uruchamiamy samochód itd...
delay(c, 2);
c.stop();
delay(c, 2);
c.start();
delay(c, 4);
c.stop();
System.out.println("Zużycie paliwa: " + (10 - c.getFuel()) +
" w czasie jazdy " + runTime + " sek.");
}
}
Metoda delay(...) nie tylko symuluje upływ czasu, ale również informuje jaki
jest stan pojazdu (stoi, jedzie) w tym czasie oraz zlicza czas "czystej" jazdy (zmienna
runTime). Na końcu programu pokazujemy ile zużyto paliwa w ciągu ilu sekund
jazdy.
Okaże się, że uruchamiając kilka razy ten program otrzymamy różne wyniki.
Możemy np. otrzymac i taki:
Samochód JEDZIE przez 3 sek....
Paliwo: 9
Paliwo: 8
Paliwo: 7
Samochód STOI przez 1 sek....
Samochód JEDZIE przez 2 sek....
Paliwo: 6
Paliwo: 5
Samochód STOI przez 2 sek....
Paliwo: 4
Samochód JEDZIE przez 4 sek....
Paliwo: 3
Paliwo: 2
Paliwo: 1
Paliwo: 0
Nie jest mozliwe przejscie ze stanu STOI do stanu STOI
Zużycie paliwa: 10 w czasie jazdy 9 sek.
Aby wytłumaczyć ten wynik - przypomnijmy fragment klasy Car:
public void stop() {
fuelConsume = null;
super.stop();
}
public void run() {
Thread cThread = Thread.currentThread();
while(cThread == fuelConsume) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
fuel--;
System.out.println("Paliwo: " + fuel); // śledzimy ile jest paliwa
if (fuel <= 0) stop(); // jeżeli brak paliwa...zatrzymujemy samochód
}
}
Jak widać, wątek zużycia paliwa usypia na sekundę. W tym czasie może się
zdażyć, że główny wątek - za pomocą metody stop(), użytej wobec obiektu klasy
Car zatrzymał samochód. Po obudzeniu wątek zużycia paliwa nic o tym nie wie
i wykonuje operację fuel-- (zmniejszając paliwo, mimo, że samochód stoi).
Wydaje się, że ta sytuacja wynika z braku synchronizacji. Istotnie, mamy
tu do czynienia ze zmienną wspóldzieloną przez dwa wątki (fuelConsume).
Synchronizowanie w metodzie stop() jest oczywiste:
synchronized(this) {
fuelConsume = null;
}
w metodzie run() wymaga natomiast lekkiej przebudowy: w czasie kiedy wątek
zużycia paliwa odlicza sekundę i zmniejsza paliwo, żaden inny wątek nie powinien
mieć dostępu do zmiennej fuelConsume.
public void run() {
Thread cThread = Thread.currentThread();
while(true) {
synchronized(this) {
if (cThread != fuelConsume) break;
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
fuel--;
System.out.println("Paliwo: " + fuel); // śledzimy ile jest paliwa
}
if (fuel <= 0) stop(); // jeżeli brak paliwa...zatrzymujemy samochód
}
}
Efekt ten uzyskamy dzięki właściwości metody sleep(..).
Użycie metody sleep(...) przesuwa wątek do stanu NOT RUNNABLE, ale nie zwalnia zamkniętych rygli
Skądinąd, właściwość ta jest jednak niebezpieczna, może bowiem spowodować
niepotrzebne, zbyt długie, oczekiwanie innego wątku na ryglu zamkniętym przez
śpiący wątek.
Właśnie dlatego prezentowana wyżej synchronizacja nie rozwiąże naszego problemu:
nadal możemy otrzymywać wyniki zużycia 10 litrów nbenzyny w 9 sekund jazdy.
W kontekście np.
delay(c, 2);
c.stop();
scenariusz może być taki:
po obudzeniu głównego wątku (uśpionego w metodzie delay) główny watek nie
otrzymuje od razu czasu procesora, wykonuje się natomiast kolejna iteracja
pętli wątku zużycia paliwa. Wprowadzana jet znowu sekcja krytyczna i następuje
uśpienie wątku zużycia paliwa. Kontrolę zyskuje wątek główny, ale metoda
stop() czeka na zaryglowanym obiekcie. Wątek zużycia paliwa budzi się po
sekundzie, zmniejsza paliwo i po wyjściu z sekcji krytycznej oddaje sterowanie
wątkowi głównemu Dopiero teraz wykonywane jest stop(), które przerwie działanie
wątku zużycia paliwa. W takiej sytuacji łatwo zgubić "trochę" paliwa.
Skoro synchronizacja nie rozwiązuje problemu nadmiernego zużycia paliwa, to spróbujmy poradzić sobie inaczej,
Przede wszystkim zadeklarujemy zmienną fuelConsume jako volatile (aby zapewnić
uzgadnianie przez watki ich kopii z oryginałem przy każdym odwołaniu do zmiennej).
volatile private Thread fuelConsume;
Powodem naszych kłopotów było pierwotnie (kiedy jeszcze nie zapewnialiśmy
wykluczania) to, iż w czasie uśpeinia wątku zużycia paliwa wątek główny mógł
wykonać metodę stop() i nadać zmiennej fuelConsume wartośc null, a po obudzeniu
wątek zużycia paliwa nie sprawdzał tego i zmniejszal paliwo. Zatem sprawdźmy
czy po obudzeniu wątku zużcyia paliwa przyapadkiem samochód nie jest już
zatrzymany.
public void run() {
Thread cThread = Thread.currentThread();
while(cThread == fuelConsume) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
if (cThread == fuelConsume) { // zmniejszamy paliwo tylko jeśli nie wykonano stop
fuel--;
System.out.println("Paliwo: " + fuel); // śledzimy ile jest paliwa
}
if (fuel <= 0) stop(); // jeżeli brak paliwa...zatrzymujemy samochód
}
}
Niestety i tym razem wynik nie będzie dobry. Może się okazać, że w ciągu 9 sekund jazdy zużyto tylko 6 lub 7 litrów paliwa. Np.
Samochód JEDZIE przez 3 sek....
Paliwo: 9
Paliwo: 8
Samochód STOI przez 1 sek....
Samochód JEDZIE przez 2 sek....
Paliwo: 7
Samochód STOI przez 2 sek....
Samochód JEDZIE przez 4 sek....
Paliwo: 6
Paliwo: 5
Paliwo: 4
Paliwo: 3
Zużycie paliwa: 7 w czasie jazdy 9 sek.
Zauważmy, że w naszym programie samochodowym dwa wątki usypiają co pewien
czas na sekundę. Po obudzeniu nie ma gwarancji, że wątek dostanie natychmiast
czas procesora (do którego konkuruje wiele innych wątków z różnych przecież
procesów). Może się więc zdarzyć, że wątek zużycia paliwa nie dostanie czasu procesora zaraz po obudzeniu, a wątkowi głównemu
(w którym wykonywana jest metoda delay()) "równolegle" uda się zwiększyć
licznik upływającego czasu. Efektem jest zbyt małe z kolei zużycie paliwa w stosunku do czasu jazdy.
Powinniśmy więc w bardziej subtelny sposób próbować kontrolować czas przydzielany poszczególnym wątkom.
Mamy po temu dwie możliwości:
- zmianę priorytetów wątków,
- zapewnienie, by wybrany wątek częściej dobrowolnie oddawał czas procesora.
Priorytet wątku jest liczbą całkowitą. Wątki o wyższym priorytecie powinny
od systemowego zarządcy wątków uzyskiwać częstszy dostęp do procesora
Priorytety wątków odgrywają rolę przy ich szeregowaniu.
Szeregowaniem wątków nazywa się ustalenie kolejności wykonania (dostępu do procesora) wielu wątków na maszynie jednoprocesorowej
JVM stosuje deterministyczny algorytm szeregowania wątków zwany szeregowaniem
stało-priorytetowym i bazującym na priorytetach wątków.
Gdy uruchamiamy nasz program, tworzony jest i uruchamiany jego wątek główny i w nim wykonywana jest metoda main(...). Wątek ten ma priorytet oznaczony stałą Thread.NORMAL_PRIORITY (obecnie 5).
W wątku głównym możemy tworzyć inne wątki, a w tych innych - jeszcze inne. Gdy tworzony jest nowy watek - uzyskuje on ten sam priorytet, co wątek w którym został stworzony.
Po utworzeniu wątku możemy zmienić jego priorytet za pomocą metody setPriority(int).
Dostępny zakres priorytetów obejmuje liczby z przedziału Thread.MIN_PRIORITY... Thread.MAX_PRIORITY (obecnie 1-10).
Po utworzeniu wątku, wątek znajduje się w stanie NEW THREAD i nie jest jeszcze gotowy do wykonania. Dopiero użycie metody start() na rzecz wątku przeprowadza go w stan gotowości do wykonania - zwany RUNNABLE. Zob. stany wątków.
Bycie w stanie RUNNABLE nie znaczy, że wątek się wykonuje. Może
on czekać w kolejce innych wątków na przydzielenie czasu procesora przez
zarządcę wątków
Gdy w danym momencie czasu wiele wątków gotowych jest do wykonania (czeka
w kolejce na czas procesora) zarządca wątków wybiera wątek z najwyższym priorytetem
i jemu przydziela czas.
Gdy czekają wątki o tym samym priorytecie - wybierany jest jeden z nich.
Wybrany wątek wykonuje się dopóki:
- nie zwolni procesora na skutek uśpienia (metoda sleep), dobrowolnego
oddania czasu (metoda yield()), lub innych przyczyn (o których mowa była przy omawianiu
stanów wątków),
- wątek o wyższym priorytecie nie będzie gotowy do wykonania,
- nie upłynie przydzielony mu kwant czasu procesora (jeśli system operacyjny zapewnia wywłaszczanie po upływie kwantu czasu).
Następnie zarządca wątków przydziela czas kolejnemu wątkowi z kolejki oczekujących na czas procesora. I tak "w koło Macieju".
Możemy zatem spróbować zapewnić właściwe (cosekundowe) zużycie paliwa ustalając
dla wątku zużycia paliwa priorytet wyższy od priorytetu wątku z którego zostaje
on uruchomiony.
Do ustalania priorytetów watków sluży metoda klasy Thread
setPriority(int prior)
gdzie prior - priorytet wątku
Priorytet danego wątku możemy odczytać natomiast za pomocą metody getPriority().
Ustalmy maksymalny priorytet. Istotne fragmenty kodu pokazano poniżej:
class Car extends Vehicle implements Runnable {
// ...
volatile private Thread fuelConsume;
// ..
// Start samochodu
public void start() {
if (fuel > 0) {
super.start();
if (fuelConsume == null) {
fuelConsume = new Thread(this);
fuelConsume.setPriority(Thread.MAX_PRIORITY);
fuelConsume.start();
}
}
else System.out.println("Brak paliwa");
}
// Zużycie paliwa
public void run() {
Thread cThread = Thread.currentThread();
while(cThread == fuelConsume) {
try {
Thread.sleep(1000);
} catch(InterruptedException exc) { return; }
if (cThread == fuelConsume) {
fuel--;
System.out.println("Paliwo: " + fuel);
if (fuel <= 0) stop();
}
}
}
// Zatrzymanie samochodu
public void stop() {
fuelConsume = null;
super.stop();
}
// ...
}
a wynik - przynajmniej na platformach Windows 98 i OS/2 - będzie nareszcie precyzyjny:
Samochód JEDZIE przez 3 sek....
Paliwo: 9
Paliwo: 8
Paliwo: 7
Samochód STOI przez 1 sek....
Samochód JEDZIE przez 2 sek....
Paliwo: 6
Paliwo: 5
Samochód STOI przez 2 sek....
Samochód JEDZIE przez 4 sek....
Paliwo: 4
Paliwo: 3
Paliwo: 2
Paliwo: 1
Zużycie paliwa: 9 w czasie jazdy 9 sek.
Nie możemy jednak twierdzić, że jest to uniwersalne rozwiązanie.
Wszystko zależy od tego w jaki sposób systemowy zarządca wątków przydziela
im czas procesora oraz czy i na ile uwzględnia priorytety wątków ustalane przez Javę.
Np. w różnych systemach operacyjnych są różne liczby priorytetów. Priorytety
Javy są odwzorywywane na priorytety uwzględniane przez systemowego zarządcę
watków, co może prowadzić do innego od oczekiwanego (na poziomie programu
napisanego w Javie) szeregowania wątków.
Wobec tego:
W progaramch wieloplatformowych należy unikać stosowania priorytetów wątków.
Ustalanie priorytetów może być natomiast pomocne przy szczegółowym dostrajaniu
działania programu na danej platformie systemowej
Czy istnieje inny niż zastosowanie priorytetów sposób rozwiązania naszego problemu?
Wydaje się, że podobne efekty - ale już niezależne od sposobu systemowego
odwzorowania priorytetów - możemy osiągnąć poprzez bardziej precyzyjne kwantowanie
czasu i sprawienie, by wątek, który uznajemy za mniej "krytyczny" częściej
dobrowolnie oddawał czas procesora.
W naszym programie samochodowo-paliwowym będzie to wątek główny (bo kłopoty
wynikały z tego, że wątek zużycia paliwa "nie nadążał" ze zmniejszaniem jego
ilości).
Wątek główny dobrowolnie oddaje czas na skutek wywołania metody sleep z metody
delay. Usypia na sekundę, zatem co sekundę następuję jego obudzenie i wobec
tego co sekundę następuje też oddanie przez niego czasu procesora.
A przecież w metodzie delay możemy zliczać sekundy za pomocą bardziej precyzyjnego kwantowania
czasu, na przykład dzieląc sekundę na 10 części. Wtedy dziesięć razy częściej
wątek zużycia paliwa będzie miał szansę dostępu do procesora, bowiem w ciągu
sekundy nie raz, a dziesięć razy wątek główny będzie przesuwany do stanu
NOT RUNNABLE.
Zatem: usuwamy ustalenie wysokiego priorytetu wątku zużycia paliwa z metody
start() klasy Car, a zamiast tego modyfikujemy metodę delay(), która teraz
będzie powodowac dziesięciokrotne w ciągu sekundy dobrowolne wywłaszczenie
watku głównego.
static int runTime;
static void delay(Car c, int sek) {
if (c.getState() == Car.MOVING) runTime += sek;
System.out.println(c + " przez " + sek + " sek....");
int n = sek*10;
while(n-- > 0) {
try {
Thread.sleep(100);
} catch (Exception exc) { }
}
}
Okaże się, że teraz możemy także uzyskać dokładne wyniki, dotyczące zużycia paliwa.
Ale znowu, taka granulacja czasu, dostrajanie kwantów może zależeć od systemu operacyjnego.
Można wszakże mieć nadzieję, że omówione tu sposoby postępowania okażą się przydatne przy rozwiązywaniu praktycznych problemów.
4. 9. Demony i grupy
Demony - to wątki, których przeznaczeniem jest wykonywanie swoistych prac systemowych lub
pomocniczych w tle. Podstawową różnicą pomiędzy zwykłymi wątkami i wątkami-demonami
jest to, iż program nie może skończyć działania w sposób naturalny o ile
działa jeszcze jakiś zwykły wątek (tzw. watek uzytkownika, user thread),
natomiast wątki-demony automatycznie kończą działanie wraz z zakończeniem
programu i nie wstrzymują zamknięcia aplikacji,
Do ustalenia rodzaju wątku służy metoda setDaemon(boolean), która musi być wywołana przed uruchomieniem wątku,
Wątki są łączone w grupy. Każdy wątek należy do jakiejś grupy. Możemy
też tworzyć nowe grupy wątków i dołączac do tych grup nowotworzone watki.
Służy do tego klasa ThreadGroup (oraz argument konstruktora klasy Thread).
Możemy uzyskiwać wątki należące do grupy i wykonywać różne opercaje "od razu"
na wszystkich wątkach grupy.
Zobaczmy na przykładzie. Poniższy program pokazuje wszystkie wątki biężacej
grupy, po czym kończy wszystkie wątki, które pochodzą od omówionej wcześniej
klasy InterruptibleRepetitiveThread (zob. punkt. 14.9) i znowu pokazuje wszystkie
wątki. Przy okazji prześledzimy dokładnie wątki tworzone w programie i zobaczymy
dlaczego użycie dialogów JOptionPane wymaga kończenia aplikacji za pomocą
System.exit().
import javax.swing.*;
class ThreadReporter {
// Jako argument podajemy tryb, określający czy mamy
// korzystać z elementó GUI
// od tego trybu zależy ile wątków będzie działać
// w naszym programie
public ThreadReporter(String mode) {
if (mode.equals("Frame")) {
JFrame f = new JFrame();
}
else if (mode.equals("Dialog"))
JOptionPane.showMessageDialog(null, "Zobaczmy!");
showThreads();
try { Thread.sleep(3000); } catch(Exception exc) {}
tryToInterruptAllInterruptible();
try { Thread.sleep(3000); } catch(Exception exc) {exc.printStackTrace();}
showThreads();
}
// Metoda zwraca tablicę aktywnych wątków w bieżącej grupie
public Thread[] getActiveThreads() {
ThreadGroup tgr = Thread.currentThread().getThreadGroup();
// Uwaga: activeCount() daje estymowaną liczbę wątków w grupie
Thread[] t = new Thread[tgr.activeCount()];
// Metoda enumerate - wypelnia tablicę wątków
int n = tgr.enumerate(t, true);
// Działające wątki
Thread[] realThreads = new Thread[n];
for (int i=0; i<n; i++) realThreads[i] = t[i];
return realThreads;
}
// Pokazuje informacje o dizłalających wątkach
public void showThreads() {
Thread[] t = getActiveThreads();
System.out.println("Liczba aktywnych wątków " + t.length);
for (int i=0; i < t.length; i++) {
System.out.println(t[i].getName() + ", priority = " +
t[i].getPriority() +
(t[i].isDaemon() ? " daemon " : " user ") + "thread"
);
}
}
// Kończy wszystkie wątki, które należą do klasy
// InterruptibleRepetitiveThread
public void tryToInterruptAllInterruptible() {
Thread[] t = getActiveThreads();
for (int i=0; i < t.length; i++) {
if (t[i] instanceof InterruptibleRepetitiveThread)
t[i].interrupt();
}
}
public static void main(String[] args) {
String s = (args.length == 0 ? "none" : args[0]);
new InterruptibleRepetitiveThread("Gwiazdki", 1000,
new Command() {
public void execute() {
System.out.println("***");
}
}).start();
new InterruptibleRepetitiveThread("Plusy", 1000,
new Command() {
public void execute() {
System.out.println("+++");
}
}).start();
new ThreadReporter(s);
}
}
W zależności do tego z jakim argumentem uruchomimy program - zostanie stworzona
różna liczba wątków. Jeśli nie podamy żadnego argumentu, będziemy mieli
- oprócz głównego (o nazwie main) - tylko dwa własne wątki "Gwiazdki" i "Plusy", po zakończeniu których aplikacja zakończy wątek główny i zakończy działanie:
Liczba aktywnych wątków 3
main, priority = 5 user thread
Gwiazdki, priority = 5 user thread
Plusy, priority = 5 user thread
+++
***
***
+++
Plusy - interrupted
Gwiazdki - interrupted
Liczba aktywnych wątków 1
main, priority = 5 user thread
Jeśli podamy argument Frame, to zostanie stworzone okno JFrame, co spowoduje
(niejawnie) utworzenie dwóch dodatkowych wątków, związanych z GUI.
Liczba aktywnych wątkąw 5
main, priority = 5 user thread
Gwiazdki, priority = 5 user thread
Plusy, priority = 5 user thread
AWT-Shutdown, priority = 5 user thread
AWT-Windows, priority = 6 daemon thread
***
+++
***
+++
***
+++
Gwiazdki - interrupted
Plusy - interrupted
Liczba aktywnych wątków 2
main, priority = 5 user thread
AWT-Windows, priority = 6 daemon thread
Aplikacja również zakończy działanie, gdyż wątek AWT-Shutdown zamyka ją i
kończy wykonanie, a pozostający AWT-Windows jest demonem i automatycznie
kończy się wraz z zakończeniem wątku głownego.
Wreszcie - gdy za pomocą argumentu - Dialog, spowodujemy otwarcie dialogu
JOptionPane, to pojawią sie jeszcze dwa dodatkowe wątki: AWT-EventQueue (do
obsługi
kolejki zdarzeń) oraz ImageFetcher (do pobierania obrazów, co jest potrzebne
bo dialog zawiera ikonę). Teraz, po zamknięciu dialogu wątek ImageFetcher
automatycznie zakończy działanie, a nasze wątki zostaną zakończone przez
naszą metodę tryToInterrupt...
Jednak ponieważ wśród pozostałych wątków znajduje się wątek AWT-EventQueue,
który nie jest demonem, to po zakończeniu wątku głownego nasza aplikacja
nie skończy dzialania.
Liczba aktywnych w¦tkˇw 7
main, priority = 5 user thread
Gwiazdki, priority = 5 user thread
Plusy, priority = 5 user thread
AWT-Shutdown, priority = 5 user thread
AWT-Windows, priority = 6 daemon thread
Image Fetcher 0, priority = 8 daemon thread
AWT-EventQueue-0, priority = 6 user thread
***
+++
***
+++
+++
***
Gwiazdki - interrupted
Plusy - interrupted
Liczba aktywnych w¦tkˇw 4
main, priority = 5 user thread
AWT-Shutdown, priority = 5 user thread
AWT-Windows, priority = 6 daemon thread
AWT-EventQueue-0, priority = 6 user thread
Dlatego wlaśnie w takich przypadkach musimy używać System.exit(..).
4. 10. Timery
Klasa Timer służy do uruchamiania zadań, określanych przez obiekty klasy
TimerTask w określonym czasie i/lub z określoną częstotliwością.
Zdefiniowanie zadania do wykonania polega na odziedziczeniu klasy TimerTask
i dostarczeniu kodu do wykonania w metodzie run(). Następnie za pomoca metody
schedule klasy Timer możemy zlecić, by kod ten został wykonany albo o określonym
czasie, albo by jego wykonanie powtarzało sie z określoną częstostliwością,
albo połaczyć obie te możliwości: rozpoczęcie zadania w określonym czasie
i powtarzanie go z określoną częstotliwością. Odwołanie zadania do wykonania
lub zakończenie wykonującego się zadania (jeśli jest to zadanie powtarzające
się) uzyskujemy za pomocą wywołania na jego rzecz metody cancel().
Obrazuje to poniższy przykładowy program.
import java.util.*;
// Klasa definiująca zadanie do wykonania
// tu będzie to wypisywanie komunikatów
class Message extends TimerTask {
private String msg; // komunikat
private boolean showDate; // czy pokazać datę
public Message(String s, boolean show) {
msg = s;
showDate = show;
}
// Ten kod będzie wykonywany przez Timer
// według charakterystyk czasowych podanych w metodzie schedule
public void run() {
String msg1 = msg;
if (showDate) msg1 = "Jest " + new Date() + '\n' + msg;
System.out.println(msg1);
}
}
// Test
public class Timer1 {
public static void main(String[] args) {
// Utworzenie zadań
Message msgTask1 = new Message("Przypominam o podlaniu kwiatów!", true),
msgTask2 = new Message("I zakończ ten program!", false);
// Czas rozpoczęcia drugiegi zadania:
// za trzy sekundy od teraz
long taskTime2 = System.currentTimeMillis() + 3000;
// Utworzenie timera
Timer tm = new Timer();
// Zlecenie pierwszego zadania do wykonania
// Ma się zacząć JUŻ - podano 0 jako moment startu
// i powtarzać się co 2 sekundy
tm.schedule(msgTask1, 0, 2000);
// Zlecenie drugiego zadania do wykonania
// Zacznie się w momencie określanym przez datę
// - utworzony obiekt Date; tu - zgodnie z czasem timeTask2
// ale mogłoby to być np. 11 lipca o 9 rano
tm.schedule(msgTask2, new Date(taskTime2), 2000);
// Po twierdzącej odpowiedzi na to pytanie ...
int rc = javax.swing.JOptionPane.showConfirmDialog(null,
"Czy kwiaty podlane?");
// ... kończymy działanie zadania msgTask1
// do czego służy metoda cancel():
if (rc == 0) msgTask1.cancel();
// zadanie 2 nadal działa
javax.swing.JOptionPane.showMessageDialog(null, "Kończyć trzeba");
// teraz kończymy wszystko
System.exit(0);
}
}
Działanie programu ilustruje rysunek.
Klasa Timer może okazać się bardzo użyteczna: dostarcza różnych strategii
określania częstotliwości powtórzeń (np. czy - przy niedokładnym przecież
pomiarze czasu środkami programowymi - preferować zachowanie stałych odstępów
czasowych między wykonaniem zadania czy też może dążyć do utrzymania sumarycznego
czasu wykonania, wynikającego z liczby powtórzeń). Jest także dobrze skalowalna:
działanie Timera jest takie samo przy małej jak i dużej (idącej w tysiące)
liczbie kontrolowanych przez niego wątków-zadań,
4.11. Pułapki
Problem: samolubne i zagłodzone wątki
Co się stanie, jeśli w metodzie run(), definiującej działanie wątku zawrzemy pętlę wykonującą sie kilka milionów razy?
Czy inny wątek - w trakcie działania pętli - będzie miał szansę dostępu do procesora?
Może nie mieć, gdy ten długo wykonujący się wątek zawłaszczy czas procesora.
Wątek zawłaszczający nazywa się "asmolubnym", a ten, ktory nigdy nie ma szansy dostępu do procesora "zagłodzonym".
Możemy manipulować priorytetami, albo liczyć na systemowe wywłaszczanie...
Ale to może nic nie dać (np. jeśli system nie podtrzymuje wywłaszczania).
Zagłodzenie jakiegoś wątku może pojawić się także na skutek określonej konfiguracji
działania kilku nawet z pozoru bezpiecznych wątków.
Rozwiązanie
Zapewnić dobrowolne wywłaszczanie się wątków:
- za pomocą wywołania metody sleep(...). Uśpienie wątku, nawet na krótko
(np. 1/10 sek.) daje szanse dostępu do procesora innym wątkom.
- poprzez zastosowanie metody yield() (np. po wykonaniu jakiejś częsci
zadania wątku), która to metoda powoduje oddanie czasu procesora innemu oczekującemu
wątkowi, który ma wyższy lub taki sam priorytet
Problem: zakleszczenie
Sytuacja: wątek A wywołuje na rzecz obiektu x synchronizowaną metodę mx. Obiekt x zostaje zaryglowany.
W tym samym czasie wątek B wywołuje na rzecz obiektu y synchronizowaną metodę my. Obiekt y zostaje zaryglowany.
W metodzie mx występuje wywołanie jakiejś innej metody sybchronizowanej na
rzecz obiektu y. Ale obiekt jest zaryglowany i wątek A czeka na jego odryglowanie.
W metodzie my występuje wywołanie jakiejś metody synchronizowanej na rzecz
obiektu x. Ale obiekt x jest zaryglowany i wątek B czeka.
Oba wątki będą czekać w nieskończoność.
Rozwiązanie
Odpowiednie projektowanie programów.
Wiele informacji na ten temat zawiera np. książka Gruźlewski, Weiss, "Programowanie
współbieżne i rozproszone w przykładach i zadaniach", WNT 1993.
4.10. Podsumowanie
Prgramowanie współbieżne jest trudną sztuką. Programy wielowątkowe mogą
wykonywać się różnie na róznych platformach systemowych, a nawet na tej samej
platformie, bowiem ich wykonanie zależy od nie dających się z góry przewidzieć
momentów i kolejności otrzymywania i oddawania czasu procesora.
Każdy wątek może być odsunięty od procesora praktycznie w dowolnym miejscu
wykonywanego kodu, w związku z czym współdziałanie wątków na tych samych
obiektach może prowadzić do niespójnych stanów obiektów.
Najważniejsza zasada programowania współbieżnego i wielowątkowego, powtarzana
przez najwybitniejszych specjalistów z tej dziedziny brzmi:
Należy - jak tylko się da i kiedy tylko można- unikac porgramowania współbieżnego.
Jeśli zaś jesteśmy do tego zmuszeni, to należy zadbać o omówione w niniejszym wykładzie:
- właściwe kończenie i wstrzymywanie pracy wątków,
- właściwą synchronizację watków, pozwalajacą na bezpieczne wspóldzielenie przez nie zasobów,
- właściwą koordynację działania wątków, zapewniającą odpowiednią kolejność działań,
- samowywłaszczanie wątków,
- odpowiednei kwantowanie czasu - jeśli wątki wykonują czasowo zależne zadania.