"VypisovačKočky: zámek píšeKočka obdržen"); System.out.println("Kočka leze dírou"); // ... while(!Program.píšePes.napsáno) { synchronized(Program.píšePes) { Program.žurnál.log(Level.INFO, "VypisovačKočky: žádám zámek píšePes"); Program.píšePes.wait(); Program.žurnál.log(Level.INFO, "VypisovačKočky: vracím zámek píšePes"); } } // ... Program.žurnál.log(Level.INFO, "VypisovačKočky: vracím zámek píšeKočka"); } } catch(InterruptedException iex){} }
Podobnou úpravu třídy VypisovačPsa jistě zvládnete sami. Po spuštění dostaneme výstup (pro stručnost vynecháme informace o čase výpisu): INFO: INFO: INFO: INFO: INFO: INFO:
VypisovačPsa: žádám zámek píšePes VypisovačKočky: žádám zámek píšeKočka VypisovačPsa: zámek píšePes obdržen VypisovačKočky: zámek píšeKočka obdržen VypisovačPsa: žádám zámek píšeKočka VypisovačKočky: žádám zámek píšePes
Je zřejmé, že skutečně došlo k uváznutí – program čeká na obsazené zámky, které nemohou být uvolněny. Ve skutečnosti jsme zde v podstatě přesně naprogramovali scénář popsaný ve 4. kapitole v příkladu 4.2: Dva podprocesy požadují dva stejné zámky, ovšem v opačném pořadí. Každý z nich dostane jeden a pak do nekonečna čeká na druhý. Všimněte si, že zde není možné pořadí zámků změnit. Je ale možné synchronizovat pouze vybrané příkazy a tím se požadavku dvou zámků vyhnout. Zdrojový kód tohoto příkladu v původní podobě a s výpisem najdete v souborech ke stažení v adresářích 05\03-uváznutí a 05\03-uváznutí-s-výpisem.
PŘÍKLAD 5.5 | STŘÍDÁNÍ ŘÁDEK PŘI VÝPISU ŘEŠENÍ ÚLOHY N DAM Jako složitější příklad komunikace podprocesů napíšeme program, který bude vypisovat všechna řešení úlohy n dam tak, že vypíše jedno řešení začínající na nulté řádce, pak jedno řešení začínající na první řádce atd. Po vyčerpání všech řádek začne zase od nulté – vypíše druhé řešení, které na ní začíná, pak vypíše druhé řešení začínající na první řádce atd., dokud nevypíše všechna řešení. Přitom pro výpis každé skupiny řešení začínajících na stejné řádce použijeme samostatný podproces a tyto podprocesy si budou navzájem předávat řízení. Připomeňme si, že třída Řešitel k tomu poskytuje nástroj v podobě metody najdiDalšíŘešení OdŘádky(), které zadáme číslo řádky, a ona nám vrátí logickou hodnotu říkající, zda se takové řešení podařilo najít; nalezené řešení pak vypíšeme pomocí metody výpis(). 98 Java – programování podprocesů (vláken)
Ukázka elektronické knihy, UID: KOS505707
Protože předem nevíme, kolik dam – a tedy kolik vypisujících podprocesů – budeme mít, použijeme trochu jiný přístup než v předchozím příkladu: ◾◾ Všechny vypisovací podprocesy budou po vytvoření čekat na vyrozumění, že mohou začít něco dělat. ◾◾ Hlavní podproces bude postupně spouštět jednotlivé vypisovací podprocesy. ◾◾ Vypisovací podproces po spuštění vypíše další řešení (pokud existuje), vyrozumí hlavní podproces, že skončil, a bude čekat na další spuštění. Přitom ovšem musíme vzít v úvahu, že počet řešení začínajících v i-té řádce se může pro různá i lišit a pro některá i nemusí existovat dokonce žádné řešení, které by v ní začínalo. (Například úloha 4 dam má pouze dvě řešení, jedno začíná v 1. a jedno ve 2. řádce.) K signalizaci vypisovacím podprocesům použijeme instanci jednoduché třídy, kterou pojmenujeme opět Dispečer a která bude obsahovat dvě veřejně přístupné datové složky, do nichž uložíme informace, zda je má podproces ještě čekat a zda řešitel našel nějaké další řešení.20 public class Dispečer { public boolean mámČekat = true; public boolean ještěJeCoPočítat = true; }
Vedle toho deklarujeme ještě pomocnou třídu HlavníDispečer, jejíž instance bude sloužit k signalizaci hlavnímu podprocesu, že je možné spustit výpis další řádky. Ta pro nás nebude překvapením: class HlavníDispečer { public boolean můžeš = true; }
Ve třídě Program, jež obsahuje metodu main(), deklarujeme veřejně přístupnou statickou datovou složku public static final HlavníDispečer hlavníDispečer = new HlavníDispečer();
Třída Vypisovač, jež bude představovat vypisovací podproces, bude mít podobu public class Vypisovač implements Runnable { private Řešitel řešitel; private int početDam; private int počátečníŘádka; Thread podproces; Dispečer dispečer; public Vypisovač(int početDam, int počátečníŘádka, Dispečer dispečer) { this.početDam = početDam; this.počátečníŘádka = počátečníŘádka; this.dispečer = dispečer; řešitel = new Řešitel(početDam); podproces = new Thread(this);
20 V souborech ke stažení obsahuje třída Dispečer ještě další složky, které se hodily při ladění, a to číslo počáteční řádky a vhodně překrytou metodu toString(). Zde je nebudeme uvádět, protože se fungování výsledného programu nijak netýkají.
Komunikace mezi podprocesy 99
Ukázka elektronické knihy, UID: KOS505707
}
podproces.setDaemon(true); podproces.start();
private void upozorniHlavního() { DámyNaStřídačku.hlavníDispečer.můžeš = true; synchronized(DámyNaStřídačku.hlavníDispečer) { DámyNaStřídačku.hlavníDispečer.notify(); } } @Override public void run() { try { while(řešitel.najdiDalšíŘešeníOdŘádky(počátečníŘádka)) { while(dispečer.mámČekat) { synchronized(dispečer){ dispečer.wait(); } } dispečer.mámČekat = true; řešitel.výpis(); upozorniHlavního(); } dispečer.ještěJeCoPočítat = false; upozorniHlavního(); } catch(InterruptedException iex){} } }
Konstruktor uloží do datových složek předané parametry – počet dam, požadovanou řádku a dispečera pro tento podproces. Pak vytvoří instanci řešitele (každý podproces bude mít svou), vytvoří z aktuální instance podproces, udělá z něj démona a tento podproces spustí. Hlavní práci obstarává jako vždy metoda run(). V cyklu vždy nejprve najdeme další řešení a pak počkáme, až přijdeme na řadu. Jakmile nám dispečer povolí pokračovat, nastavíme dispečerovi příznak, že má čekat, vypíšeme řešení a upozorníme hlavního dispečera, že řešení bylo vypsáno. Pak opět zkusíme najít další řešení, budeme čekat na svém dispečerovi atd.21 Pokud neexistuje další řešení začínající na zadané řádce, vrátí metoda najdiDalšíŘešeníOdŘádky() hodnotu false a cyklus skončí. V tom případě nastavíme v dispečerovi příznak, že byla vypsána všechna řešení, a upozorníme hlavního dispečera, že jsme skončili. Celý cyklus je uzavřen do příkazu try, neboť v metodě upozorniHlavního() voláme metodu wait(), z níž se může rozšířit výjimka. Ve třídě Program definujeme – vedle složky hlavníDispečer, o níž jsme hovořili na počátku tohoto příkladu – statické datové složky 21 Zde předpokládám, že jste metodu výpis() ve třídě Řešitel upravili tak, jak jsme si to řekli v závěru příkladu 2.1. 100 Java – programování podprocesů (vláken) Ukázka elektronické knihy