Dione
Z. KotalaP. Toman: Java
Predchozi (Obsah) Dalsi

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:

vlakna.gif

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_PRIORITYMAX_PRIORITY (definované ve třídě Thread), kde vyšší číslo znamená vyšší prioritu. Od priority se odvíjí tato pravidla:

  1. Běžící vlákno musí mít nejvyšší prioritu.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Pozn.: 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.


  • (1) Uvedené deklarace kvůli přehlednosti neobsahují modifikátory final, native a výjimky (throws).
  • (2) K zablokování dojde, když dvě vlákna na sebe vzájemně čekají (např. až jedno druhému uvolní požadovaný prostředek). Tato problematika je bohužel značně rozsáhlá a překračuje rámec sborníku.
  • (3) Uvedené deklarace metod nejsou kvůli přehlednosti úplné, neobsahují modifikátory a nedeklarují výjimky.

Predchozi
Converted by Selathco v0.9 on 25.09.1999 19:46
Dalsi