Vlákno názorů k článku Hra Tetris naprogramovaná v Jave a Swingu od Filip Jirsák - Veškerá manipulace s objekty Swingu má být prováděna...

  • Článek je starý, nové názory již nelze přidávat.
  • 20. 8. 2019 10:03

    Filip Jirsák
    Stříbrný podporovatel

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

  • 20. 8. 2019 10:25

    KarelI

    Ten přístup ze dvou threadů je špatně, ale stačilo by to opravit tak, že se použije java.swing.Timer, který běhá v EDT.

    Můj přístup je, že z toho threadu se může udělat víc věcí, pokud je zaručeno, že to bude rychlé a nenaruší to design aplikace.

  • 20. 8. 2019 11:32

    KarelE

    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í EventDispatchThre­ad.

    Každopádně v tomhle příkladu ani jednu z těch dvou chyb nevidím. Celé to začne "EventQueue.in­vokeLater", 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 EventDispatchThre­ad, 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.

  • 20. 8. 2019 11:39

    Calculon

    Ono toto platí obecně všude, kde je smyčka událostí (resp. platilo v době vzniku Javy a její knihovny pro GUI), jenže v Javě je ten návrh tak špatný, že vývojářům hází klacky pod nohy. Dnes už se ten offloading na jiná vlákna řeší v příčetných jazycích korutinami a kooperativním schedulerem.

  • 20. 8. 2019 11:51

    KarelI

    Nějak jsem si nevšiml, že by se návrh swingu v javě (nejpozději od 1.3) nějak odlišoval od jiných běžných gui frameworků.

  • 20. 8. 2019 11:47

    KarelI

    Je tam špatné to, že se používá java.util.Timer, pro který se používá thread pool. Ten sahá na objekty gui, takže špatně.

  • 20. 8. 2019 12:05

    Filip Jirsák
    Stříbrný podporovatel

    Celé to začne "EventQueue.in­vokeLater", 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 EventDispatchThre­ad

    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í EventDispatchThre­ad.
    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.

  • 21. 8. 2019 11:58

    KarelE

    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.Compo­nent.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.

  • 21. 8. 2019 12:20

    Filip Jirsák
    Stříbrný podporovatel

    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.

  • 21. 8. 2019 13:01

    Ondrej Nemecek

    >> 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?

  • 21. 8. 2019 13:11

    Filip Jirsák
    Stříbrný podporovatel

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

  • 21. 8. 2019 15:35

    KarelE

    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.

  • 21. 8. 2019 15:52

    Filip Jirsák
    Stříbrný podporovatel

    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.

  • 21. 8. 2019 14:24

    KarelI

    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.