16. Vlákna (threads)
Java umožňuje tzv. multithreading neboli paralelní běh dvou či více
částí programu. Této vlastnosti využije například Internetový prohlížeč -
může nahrávat obrázky po síti, zároveň formátovat a zobrazovat WWW stránku
a ještě k tomu spouštět applet - nebo textový editor, který může provádět
kontrolu pravopisu, zatímco uživatel píše dokument apod.
Každá paralelně běžící část programu se v Javě nazývá vlákno (thread). Důležité je, že jednotlivá vlákna lze naprogramovat téměř
nezávisle a pouze v případě, že sdílí společná data nebo používají stejné
prostředky (zařízení), je třeba zajistit jejich řádnou synchronizaci
(viz 16.5.). Na většině dnešních počítačů s jedním procesorem se samozřejmě nejedná
o fyzicky současný běh vláken, ale jednotlivá vlákna se na procesoru střídají
- podrobnosti viz 16.4..
16.1. Třída Thread
Každé vlákno v Javě je instancí třídy Thread (z balíku java.lang) nebo jejího potomka. Tato třída definuje základní metody
jako je spuštění, zastavení a ukončení vlákna.
Jednoduché vlákno se naprogramuje tak, že se vytvoří potomek třídy Thread, který definuje metodu:
public void run()
obsahující kód, který bude prováděn paralelně. Spuštění vlákna se
provede zavoláním metody start() (metoda run() se přímo nevolá).
Priklad 16.1. |
/* třída definující vlákno */
class Vlakno extends Thread { // (1)
Vlakno(String jmeno) { // (2)
super(jmeno); // (3)
}
public void run() { // (4)
for (int i=0; i<5; i++) {
System.out.println("Vlakno: " + getName()); // (5)
yield(); // (6)
}
}
}
/* hlavní program */
public class Vlakna {
public static void main(String[] args) {
Vlakno v1 = new Vlakno("v1"); // (7)
Vlakno v2 = new Vlakno("v2"); // (8)
v1.start(); // (9)
v2.start(); // (10)
}
}
Tento program vytvoří dvě vlákna v1 (7) a v2 (8),
instance třídy Vlakno (1), a spustí je (9, 10). Tím se zahájí paralelní
provádění metod run() (4) obou vláken. Metoda run() simuluje
smysluplnou činnost tím, že 5x vypíše na výstup jméno svého vlákna (5) - bylo
předáno jako parametr konstruktoru (7, 8). Význam metody yield() (6) viz
16.4.. Výstup programu dopadne takto:
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
Vlakno: v1
Vlakno: v2
|
|
Nejdůležitější metody definované ve třídě Thread
jsou: (1)
- Veřejné konstruktory:
Thread(), Thread(Runnable r), Thread(String s),
Thread(String s, Runnable r),
Thread(ThreadGroup g, Runnable r, String s),
Thread(ThreadGroup g, Runnable r),
Thread(ThreadGroup g, String s) - parametry:
- r - reference na objekt implementující metodu run()
(viz 16.2.),
- s - řetězec reprezentující jméno vlákna,
- g - skupina, do které bude vlákno zařazeno (viz 16.7.).
- public static Thread currentThread() - vrací referenci na právě
běžící vlákno,
- public String getName() - vrací jméno vlákna,
- public int getPriority() - vrací prioritu
vlákna (viz 16.4.),
- public void join() - čeká na ukončení vlákna,
- public void resume() - probudí vlákno "odstavené" metodou suspend(),
- public void run() - jádro vlákna (def. jako prázdná metoda),
- public void start() - zahájí běh vlákna, tj. provádění metody
run(),
- public static void sleep(long ms) - uspí (dočasně
zastaví) vlákno na ms milisekund. Pak jej probudí a vlákno může
pokračuje v běhu,
- public setPriority(int priorita) - nastaví prioritu
vlákna (viz 16.4.),
- public void stop() - ukončí vlákno,
- public void suspend() - "odstaví" vlákno. Odstavené vlákno může
probudit pouze metoda resume(),
- public static void yield() - umožní běh jiného vlákna (viz
16.4.).
16.2. Rozhraní Runnable
Rozhraní Runnable (z balíku java.lang) obsahuje pouze deklaraci
metody:
public void run();
Třída pomocí tohoto rozhraní může definovat jádro vlákna, metodu
run(), přičemž sama nemusí být potomkem třídy Thread. To
je užitečné zejména v případě, že:
- program bude potřebovat pouze jedno další vlákno (kromě hlavního
programu) - je zbytečné vytvářet kvůli jedné instanci vlákna třídu,
- daná třída je potomkem nějaké třídy (případ appletu) a potřebuje mít
vlastnosti vlákna.
Priklad 16.2. |
public class Animace extends java.applet.Applet
implements Runnable {
Thread animator = null;
int xpos = 0;
public void init() {
animator = new Thread(this); // (1)
animator.start();
}
public void run() { // (2)
while(animator != null) { // (3)
repaint(); // (4)
try {
Thread.sleep(80); // (5)
} catch(InterruptedException e) {}
}
}
public void paint(java.awt.Graphics g) { // (6)
g.drawRect(xpos, 0, 20, 20);
xpos = (xpos+1) % 100;
}
}
Applet má na pozadí přehrávat animaci. Vytvoří proto vlákno
(1) (viz str.16)
konstruktorem Thread(Runnable r), kterému předá odkaz na sebe - tj. na instanci
implementující rozhraní Runnable (2). Metoda run() vykreslí fázi animace - volá metodu repaint() (4),
počká (uspí se) na 80 ms (5), a cyklus se opakuje, čímž vzniká animace -
vykreslování zajišťuje metoda paint() (6) volaná prohlížečem na
žádost metody repaint() (4).
|
|
16.3. Ze života vlákna
Každé vlákno se v daném okamžiku nachází v právě jednom z těchto stavů:
- Nové vlákno (new thread) - je stav, kdy je
vlákno vytvořeno, ale ještě nebylo spuštěno (nejsou ještě alokovány
systémové prostředky vlákna).
- "Běhuschopný" stav (runnable) - je stav po
spuštění metodou start()
. V tomto stavu se může nacházet
více spuštěných vláken, z nichž ale jen jedno je (na počítači s jedním
procesorem) právě běžící.
- "Neběhuschopný" stav (not runnable) - do
tohoto stavu se vlákno dostane, pokud:
- je uspáno metodou sleep(),
- je "odstaveno" metodou suspend(),
- čeká v metodě wait() (viz 16.5.3.),
- čeká na vstupní/výstupní zařízení.
- Mrtvé vlákno (dead thread) - je stav po ukončení metody
run() nebo po zavolání metody stop().
Přechody mezi jednotlivými stavy znázorňuje obrázek:
16.4. Plánování (scheduling)
Na počítači s jedním procesorem, který nepodporuje paralelní běh instrukcí, se
multithreading simuluje tak, že se vlákna o procesor dělí,
podle pravidel, která určuje plánovač (scheduler).Algoritmů pro plánování je celá řada. Java používá plánování podle
priority: každé vlákno má přiděleno číslo, prioritu, v rozmezí
konstant MIN_PRIORITY až MAX_PRIORITY (definované ve
třídě Thread), kde vyšší číslo znamená vyšší prioritu. Od priority se
odvíjí tato pravidla:
- Běžící vlákno musí mít nejvyšší prioritu.
- Pokud je v běhuschopném stavu vlákno s vyšší prioritou než má
vlákno běžící, je běžící vlákno tímto okamžitě vystřídáno.
- Vlákno s nižší prioritou může běžící vlákno vystřídat jen tehdy, pokud
je běžící vlákno v neběhuschopném stavu nebo je ukončeno, a zároveň
na přidělení procesoru nečeká jiné vlákno s vyšší prioritou.
- Pokud na přidělení procesoru čeká více vláken se stejnou prioritou, je
další vybráno tak, aby se postupně vystřídala všechna.
- Běžící vlákno může navíc dobrovolně poskytnout procesor jiným vláknům se
stejnou prioritou zavoláním metody yield().
Z uvedených pravidel tedy vyplývá, že běžící vlákno nemůže být "donuceno"
k vystřídání jiným vláknem se stejnou nebo nižší prioritou, ale pouze
s prioritou vyšší. Java je ovšem v tomto ohledu benevolentní a dovoluje na operačních systémech,
které to podporují (Unix, Windows95/NT), tzv. sdílení času (time
slicing), kdy se vlákna se stejnou prioritou střídají po pevně
přidělených časových intervalech. Tím se zamezí tomu, aby si například vlákno
s maximální prioritou (MAX_PRIORITY) zabralo procesor "na věčné
časy".
Priklad 16.3. |
Pokud z příkladu 16.1. odstraníme řádku (6) s metodou yield(), dopadne výstup na různých operačních systémech takto:
OS s přidělováním času: OS bez přidělováním času:
Vlakno: v2 Vlakno: v1
Vlakno: v1 Vlakno: v1
Vlakno: v2 Vlakno: v1
Vlakno: v2 Vlakno: v1
Vlakno: v2 Vlakno: v1
Vlakno: v2 Vlakno: v2
Vlakno: v1 Vlakno: v2
Vlakno: v1 Vlakno: v2
Vlakno: v1 Vlakno: v2
Vlakno: v1 Vlakno: v2
Metoda yield() totiž zaručuje, že se vlákna mohou spravedlivě
střídat, a tak i v operačním systému bez přidělování času vypadá výstup, jak
bylo uvedeno v příkladu 16.1.. Protože sdílení času není v Javě zaručeno, nedoporučuje se psát programy,
které jsou na něm závislé.
|
|
16.5. Synchronizace
Potřeba synchronizace vzniká všude tam, kde je možné (avšak nepřípustné)
používat současně zařízení (např. automat na kávu) nebo sdílet společná data
(např. skripta na analýzu).U automatu je nutné synchronizovat přístup lidí, neboť se nesmí stát, aby po
vhození mince byl člověk odstrčen jiným "uživatelem", který by pak obdržel
kýžený nápoj. Podobně skripta nemůže číst více studentů současně, jinak by při
obracení dvou stránek opačným směrem došlo brzy k jejich destrukci.
16.5.1. Kritické sekce
V programování se části kódu, jejichž paralelní běh může vyvolat
kolizi, nazývají kritické sekce. Provádění kritických sekcí je časově
závislé a pokud nejsou synchronizovány, program "někdy" nepracuje správně.
Nejjednodušším případem kritické sekce v Javě je metoda:
Priklad 16.4. |
Souborový server musí synchronizovat přístup k souborům na
disku - dva uživatelé nesmí zapisovat současně do jednoho souboru a
nepřípustné je také zapisovat do souboru, který je právě čten.
class Soubor {
private byte[] disk = {0,0}; // (1)
public void zapis(byte a, byte b) { // (2)
disk[0] = a; // (3)
disk[1] = b; // (4)
}
public byte[] cti() { // (5)
return new int[] {disk[0], disk[1]}; // (6)
}
}
Instance třídy Soubor (nazveme ji soubor) bude
představovat fyzický soubor, jehož dvoubytový obsah (1) je možné číst (5)
a zapisovat (2). K tomuto souboru budou přistupovat dvě vlákna A a B -
bude-li do souboru vlákno A zapisovat:
soubor.zapis(1,2);
a současně vlákno B z něj číst:
byte[] data = soubor.cti();
může se stát, že proměnná data bude nakonec mít obsah
{1,0} namísto očekávaného {1,2}. K přiřazení proměnné
disk totiž dojde v době "mezi" (3) a (4). Metody (2) a (5) jsou
kritické sekce.
|
|
Kritické sekce jsou vždy spojeny se společnými daty nebo zařízením.
Principiálně nic nebrání současnému běhu dvou kritických sekcí, které nesdílí data a používají rozdílná zařízení.
16.5.2. Synchronizace kritických sekcí
Vyloučení současného běhu kritických sekcí lze provést například pomocí
tzv. monitorů. Monitor se obecně skládá z dat a funkcí (metod)
jako zámků (locks) nad daty. V Javě má každý objekt vlastní
jedinečný monitor.Při vstupu do synchronizované kritické sekce vlákno získá
monitor (zámek je uzamčen) a po jejím opuštění vlákno monitor
uvolní (zámek je odemčen). Po získání monitoru jedním vláknem
nemůže jiné vlákno zahájit provádění žádné synchronizované kritické sekce
náležící k témuž monitoru (vlákno, které monitor vlastní, ano - monitory
jsou reentrantní). Synchronizovanou kritickou sekcí v Javě může být blok (viz 16.5.4.)
nebo metoda. Synchronizovaná metoda se v programu označí modifikátorem
synchronized. Každá synchronizovaná sekce je vždy provedena jako
nedělitelný (atomický) celek, bez možnosti přerušení.
Priklad 16.5. |
Metody (2) a (5) z příkladu 16.4. je pro správnou funkci třeba deklarovat takto:
synchronized public void zapis(byte a, byte b)
synchronized public byte[] cti()
Potom při libovolné sekvenci zápisů a čtení nedojde k zapsání ani
načtení nekonzistentního obsahu souboru (proměnné disk).
|
|
16.5.3. Úloha producent - konzument
Komplikovanější případ synchronizce nastává pokud dochází mezi
vlákny k předávání dat. Kromě označení kritických sekcí pomocí
synchronized se používají metody:
- public final void wait() - způsobí zastavení vlákna do
probuzení metodou notify() nebo notifyAll() operující se
stejným monitorem. Důležité je, během čekání v metodě wait() je
monitor dočasně uvolněn a může tudíž být spuštěna jiná
kritická sekce v témže objektu - to umožňuje zabránit zablokování vláken (deadlock). (2) Další verze této metody umožňují
specifikovat dobu čekání (timeout):
public final void wait(long ms), public final void wait(long ms,
int ns) (ms - počet milisekund, ns - počet nanosekund).
- public final void notifyAll() - probudí vlákna, která čekají
v metodě wait() téhož objektu a tato pak "soupeří" o pokračování
běhu (kritické sekce) na základě priority (viz 16.4.).
- public final void notify() - funguje jako notifyAll(), ale probudí jen jedno
vlákno (není zaručeno, které).
Tyto metody jsou definovány ve třídě Object (viz
11.5.) a mohou být volány pouze v synchronizovaných kritických
sekcích.
Priklad 16.6. |
Pokud by soubor z příkladu 16.4. sloužil jako (jednomístná) fronta, kam by vlákna
umisťovala ("produkovala") soubory určené k tisku - metodou zapis()
(2) - a tiskárna si je postupně vyzvedávala ("konzumovala") - metodou
cti() (5) - může se stát, že soubor bude vytištěn víckrát nebo naopak
nebude vytištěn vůbec: není totiž nijak zaručena sekvence: zápis - vybrání - zápis
- vybrání, ale může dojít například ke třem zápisům po sobě (přičemž první
dva soubory se samozřejmě ztratí). Třídu Soubor je pro tento účel třeba upravit takto:
class Soubor {
private byte[] disk = {0,0};
private boolean volno = true; // (1)
synchronized public void zapis(byte a, byte b) { // (2)
while (!volno) try { // (3)
wait(); // (4)
} catch(InterruptedException e) {}
disk[0] = a; // (5)
disk[1] = b; // (6)
volno = false; // (8)
notifyAll(); // (9)
}
synchronized public byte[] cti() { // (10)
while (!volno) try {
wait(); // (11)
} catch(InterruptedException e) {}
volno = true;
notifyAll(); // (12)
return new int[] {disk[0], disk[1]};
}
}
Klíčové jsou zde metody wait() (4,11) a notifyAll() (9,12) a
příznak volno (1), který indikuje zda je fronta volná (true) nebo
ne (false). Metoda zapis() (2) nejprve ověří, že fronta je prázdná (3) a pokud ne,
čeká v metodě wait() (4). V opačném případě zapíše data do souboru
(5,6), nastaví příznak (8) a oznámí ostatním vláknům (tiskárně) ukončení
operace - metodou notifyAll() (9), která probudí ostatní vlákna
čekající v metodě wait() (4,11) na uvolnění monitoru. Metoda cti() (10) funguje analogicky.
|
|
16.5.4. Blok synchronized
Tento blok umožňuje označit jako kritickou sekci menší část než je metoda
a určit, objekt podle jehož monitoru se bude provádět synchronizace.
Sychronizované metody totiž implicitně používají monitor instance, v jejíž
třídě jsou definovány, což někdy nemusí vyhovovat. Syntaxe bloku je:
synchronized ( referenceNaObjekt ) {
// kritická sekce
}
16.6. Démoni
Démon je vlákno, které "pouze" poskytuje služby ostatním vláknům.
Význam démonů je ten, že program, ve kterém běží pouze vlákna typu démon, může
být ukončen (viz 5.3.).Z hlediska programu je démon vlákno, které má nastaveno atribut
démona metodou:
public void setDaemon(boolean isDaemon)
s parametrem true. Obdobná metoda:
public boolean isDaemon()
vrací true, je-li vlákno démon; jinak false.
16.7. Skupiny
Skupina (group) umožňuje manipulaci s více vlákny najednou
(spuštění, ukončení atd.). Vlákno
může být při inicializaci konstruktorem (viz 16.1.) zařazeno do vlastní skupiny - jinak se automaticky stává členem skupiny implicitní
(default group).Skupiny jsou tvořeny instancemi třídy java.lang.ThreadGroup,
která definuje tyto
základní metody: (3)
- Konstruktory skupiny:
ThreadGroup(String jméno),
ThreadGroup(String jméno,
ThreadGroup rodSkupina) - parametry:
- jméno - udává jméno skupiny,
- rodSkupina - udává rodičovskou skupinu - skupiny mohou
obsahovat další (pod)skupiny,
- public int activeCount() - vrací počet vláken ve skupině,
- public String getName() - vrací jméno skupiny,
- public int getMaxPriority() - vrací maximální prioritu, kterou
může mít nově přidávané vlákno,
- public resume() - probudí všechna vlákna ve skupině odstavená
metodou suspend(),
- public void setName(String jméno) - nastaví jméno skupiny,
- public void setMaxPriority(int priorita) - nastaví maximální
prioritu, kterou může mít nově přidávané vlákno,
- public stop() - ukončí všechna vlákna ve skupině,
- public suspend() - "odstaví" všechna vlákna ve skupině.
Vlákno nelze po vytvoření přesunout do jiné skupiny, stejně tak nelze
přesunout ani podskupinu. Je tomu tak z důvodu bezpečnosti, například vlákna
každého appletu mají přidělenou svou zvláštní skupinu a nemůže se stát, aby
applet získal kontrolu nad cizím vláknem tím, že ho přesune do své skupiny.
|