Jojo, kdysi dávno jsem napsal v Javě tetris na 4kB jar do soutěže 4k game programming.
Veškerá manipulace s objekty Swingu má být prováděna ve vlákně pro event dispatch – objekty Swingu nejsou vláknově bezpečné a když s nimi budete manipulovat z jiného vlákna, bude docházet ke klasickým chybám neošetřeného vícevláknového přístupu. Naopak ve vlákně pro zpracování událostí nemá být prováděn žádný jiný kód, protože to pak vede k zamrzání GUI aplikace.
Chápu, že aplikace je pro účely článku jen takové demo, ale když už porušuje jedno ze základních pravidel pro programování ve Swingu, mělo by to být v článku alespoň napsané.
Tohle bývala (a asi nejspíš ještě je) nejčastější příčina problémů. Lidé pak nadávali, jak je GUI Javy pomalé, nestabilní a vůbec celé nedomyšlené. Jenže příčinou bylo nezvládnutí EventDispatchThread.
Každopádně v tomhle příkladu ani jednu z těch dvou chyb nevidím. Celé to začne "EventQueue.invokeLater", takže to běží ve správném vlákně. A v obsluze událostí nevidím nic, co by tam nemělo být - překresluje. Což je přesně to, co v Event Dispatch Thread být má. Pravidlo o "žádný jiný kód" neexistuje, tam je pravidlo ohledně časové náročnosti. Říká, že pokud daný kód bude trvat výrazně déle, než kolik trvá zpracování eventu, a zároveň nemění přímo obsah obrazovky, pak je třeba ho vykonat asynchronně v jiném vlákně. V článku uvedený příklad nesplňuje ani jednu podmínku: 1. Vykonat daný kód je rychlejší než předat práci do jiného threadu a pak to synchronizovat eventem v EventDispatchThread, 2. Prakticky všechny změny překreslují obrazovku, což se jinak než v EDT dělat ani nedá.
Přijde mi, že příklad v článku neporušuje žádné pravidlo.
Celé to začne "EventQueue.invokeLater", takže to běží ve správném vlákně.
Tohle je ale zásadně chybná úvaha. Celé to sice začne v invokeLater
, ale herní smyčka se spouští z Timer
u, tedy běží v jeho vláknu.
A v obsluze událostí nevidím nic, co by tam nemělo být - překresluje. Což je přesně to, co v Event Dispatch Thread být má.
Ano, překreslování má být spuštěné v Event Dispatch Thread. Tady ale běží v background thread Timer
u.
Pravidlo o "žádný jiný kód" neexistuje, tam je pravidlo ohledně časové náročnosti. Říká, že pokud daný kód bude trvat výrazně déle, než kolik trvá zpracování eventu, a zároveň nemění přímo obsah obrazovky, pak je třeba ho vykonat asynchronně v jiném vlákně.
Jenže odhadnout časovou náročnost kódu bývá dost obtížné. Milionkrát vám může databáze odpovídat svižně, a po milion prvé někdo vykopne kabel a bude se minutu čekat na timeout síťového spojení. Proto jsem to záměrně formuloval opačně – libovolné zpracování by mělo běžet v jiném vláknu, jedině pokud si programátor je opravdu jistý, může rychlé zpracování udělat ve vlákně pro zpracování událostí. Jenže takový programátor si to nepotřebuje přečíst v komentářích na Rootu :-) V tomto příkladu na první pohled opravdu nevidím nic, co by se mohlo „zaseknout“ a zdržovat zpracování událostí Swingu – ale mělo by to být v článku zmíněno, že si zde autor mohl dovolit podstatné zjednodušení a v o něco složitější aplikaci už by to musel řešit.
Pokud už to pravidlo chcete definovat časem provádění, tak je potřeba se řídit nejhorším možným časem běhu, ne nějakým ideálním, obvyklým nebo průměrným. Takže:
1. Vykonat daný kód je rychlejší než předat práci do jiného threadu a pak to synchronizovat eventem v EventDispatchThread
Nezáleží na tom, že to typicky bude rychlejší, podstatné je, zda to nemůže být (třeba v nějakém netypickém případě) pomalé.
Když se vrátím na začátek:
Tohle bývala (a asi nejspíš ještě je) nejčastější příčina problémů. Lidé pak nadávali, jak je GUI Javy pomalé, nestabilní a vůbec celé nedomyšlené. Jenže příčinou bylo nezvládnutí EventDispatchThread.
Nejčastěji to nezvládnutí vypadá tak, že si programátor řekne „to bude rychlé“, jenže u uživatele to pak z nějakého důvodu trvá déle.
Asi jsem se trochu ztratil. Co vidím já:
1. Na začátku se přepne do EDT (InvokeLater) a sestaví si GUI.
2. Vytvotvoří si java.util.Timer, který mu v pravidelných intervalech volá update() a repaint();
3. Metoda repaint se nemusí volat v EDT, protože ta jen vkládá Event do fronty. Ta je tam právě proto, aby jiné thready mohly říci AWT že až to půjde, tak ať se překreslí
4. Metoda update() nemění GUI a jediné, co volá, je zase ten repaint()
5. Samotné vykreslení je schované v doDrawing, které je volané z paintComponent. Tu volá přímo Swing v rámci EDT. Ví přesně co tam dělá a navíc je to hra - opravdu nemá smysl cokoliv z toho vyhazovat ještě do jiného threadu - nečte nic z databáze.
6. Ovládání je řešeno pomocí addKeyListener(new TAdapter()); - takže i zde dojde k volání v rámci EDT. Což je ale jedno, protože to GUI nemění.
Stále mi tedy uniká, kde přesně jsou vámi popsané chyby. To, co běží v Timeru totiž nic nepřekresluje, jen to volá metodu java.awt.Component.repaint(), která v EDT být nemusí. Ta totiž nic nepřekresluje, ale jen právě do fronty EDT přidá event na překreslení a ihned se vrátí. Takové metody tam jsou tři: repaint(), revalidate() a invalidate() - může je zavolat jakýkoliv thread.
Metoda Board.update()
mění GUI – mění text statusbaru.
Tohle je právě typický příklad – autor to bere stylem, že je to přece celé jednoduché a vlákna není potřeba řešit, a pak mu tam uteče, že se někde ve třetí zanořené metodě, kde už dávno neví, v jakém vlákně je, volá setter na Swingové komponentě.
Ví přesně co tam dělá a navíc je to hra - opravdu nemá smysl cokoliv z toho vyhazovat ještě do jiného threadu - nečte nic z databáze.
Jenže když si ten článek někdo přečte, nedozví se, že to autor (možná) pečlivě zvážil a dospěl k tomu, že si může dovolit výkonný kód volat v AWT vlákně. Čtenář naopak bude mít dojem, že nějaká vlákna vůbec řešit nemusí, protože Swing je asi sám o sobě vláknově bezpečný, když o opaku není v článku ani čárka.
>> Jenže když si ten článek někdo přečte, nedozví se, že to autor (možná) pečlivě zvážil a dospěl k tomu, že si může dovolit výkonný kód volat v AWT vlákně. Čtenář naopak bude mít dojem, že nějaká vlákna vůbec řešit nemusí, protože Swing je asi sám o sobě vláknově bezpečný, když o opaku není v článku ani čárka.
Souhlas - tím spíše, že jde o učebnicový příklad, kde se předpokládá, že se to čtenář naučí dělat koncepčně správně.
>> Tohle je právě typický příklad – autor to bere stylem, že je to přece celé jednoduché a vlákna není potřeba řešit, a pak mu tam uteče, že se někde ve třetí zanořené metodě, kde už dávno neví, v jakém vlákně je, volá setter na Swingové komponentě.
K přepnutí do jiného vlákna může ale dojít vlastně kdekoli, takže je otázka, jak lze tohle vůbec ohlídat. Tato otázka je koncepční. Mám si hlídat a nastudovat každou metodu (což je víceméně nereálné) nebo mi Swing nabízí nějaké koncepční záruky?
Swing žádné záruky nenabízí. Je potřeba to koncepčně řešit na straně aplikace – dělá se to tak, že se vytvoří model dat pro GUI, zbytek aplikace komunikuje jenom s tím modelem a model je interně udělaný tak, že pokaždé, když komunikuje se Swingem, dělá to jen z AWT vlákna. Od Javy 6 je součástí knihovny třída SwingWorker, která se dá v jednoduchých případech (jako je ten Tetris) použít, ale spíš je to jeden ze stavebních kamenů než hotové řešení.
Aha, tak to se omlouvám, já si toho statusbar.setText nevšiml. To je opravdu špatně.
Co se vyhazování do threadu týká, tak se stále neshodneme. Já se vývoji her nějaký čas věnoval a tam se opravdu update scény do jiného threadu nedává - už proto ne, že dokud není scéna upravena, tak vlastně není ani nic nového co kreslit. Ostatně proto je dodnes u her zajímavější výkon jednoho vlákna, než kolik to má jader. Pokud je článek o "hře Tetris", pak je na místě dodržovat best practices daného oboru.
Jinak jak teď koukám na tu autorovu chybu, tak se nemůžu ubránit pocitu, že ji udělal právě proto, že chtěl update scény vyhodit mimo vykreslovací smyčku. Protože ten jeho Timer vlastně dělá to, co prosazujete: update scény vyhodil do jiného vlákna a v EDT nechal jen překreslení. No a pak v tom udělal chybu. Proto by mě docela zajímalo, jakou navrhujete opravu: má Board.update přihodit do EDT? Nebo je podle vás lepší statusbar.setText nahradit atributem a do statusbaru to zapisovat v rámci doDrawing? Já osobně bych všechno hodil do EDT, respektive místo java.util.Timer použil javax.swing.Timer.
Já osobně bych především oddělil logiku hry, model GUI a samotné GUI. Jakmile máte kód, který zároveň řeší hru a zároveň GUI, nikdy to neohlídáte, aby se vše provádělo ve správných vláknech.
Chápu, že tohle mělo být jen jednoduché demo. Ale stejně tam ta informace o vláknech měla zaznít, protože je to ve Swingu podstatná věc – i kdyby tam nebyl ten statusbar a autorovi by to tedy náhodou vyšlo tak, že by vlákna nemusel explicitně řešit.
Jak už psal Filip, 4. není pravda, protože update na několika místech GUI mění.
I tento jednoduchý příklad ukazuje, že je dobré oddělit alespoň model a view s tím, že logika aplikace (v tomto případě herní) o GUI nic neví. Stačí docela primitivní přístup, kde jsou z (akce, data) vygenerována nová data, která nadále zůstanou konstantní a atomicky nahradí ta stará. Pak stačí notifikace do view, že je něco nového, ať si to přebere.
>> V tomto článku si ukážeme, ako naprogramovať klon hry Tetris v Jave a toolkitu Swing.
Jinak Swing je fajn, ale daleko současnější by bylo ten příklad udělat v JavaFX (kde to bude sice dost podobné, ale v aktuální technologii). Chci tím říct, že článek „udělej si hru ve swing“ by mi přišel aktuální před X lety...
IMHO ideální by bylo článek přepracovat se zapracováním připomínek z diskuze a udělat souběžnou verzi v JavaFX. Chápu ovšem, že to je hodně práce... Ale hezké by to bylo.
Nevím, jestli JavaFX v porovnání se Swingem označovat za „aktuální“. Ano, JavaFX je novější a v jistém smyslu modernější, na druhou stranu Swing je pořád standardní součástí Javy, zatímco JavaFX už je vyšoupnutá ven (šlo to rychleji, než jsem čekal) – takže budoucnost JavaFX není úplně jistá. Sice se JavaFX chytila komunita, ale je otázka, zda to bude stačit – nevypadá to, že by za tím byl nějaký silný hráč, který by měl na JavaFX postavené nějaké důležité systémy.
Co jsem to teď viděl?! Ten, co to psal, by s tím měl raději přestat. Jako, při vykreslování každé "kostky" vytvářet znovu pole konstantních barev nebo při nastavování tvaru opět vytváření pole konstantních koordinátů...
Tímto opravdu dáváte velice špatný příklad, jak psát kód... nehledě teda na tu Javu, ale to je kapitola sama pro sebe.