ComiaTip Swing ja säikeet


Swing ja säikeet

Kirjoittanut Hannu Julkaistu 22.5.2009

Se mitä Swing -tutoriaaleista jätetään aina pois on kuinka sen kanssa käytetään säikeitä. Aivan yksinkertaisia ohjelmia lukuunottamatta useampien säikeiden käyttö on kuitenkin käytännössä välttämätöntä. Jos esimerkiksi napin klikkauksesta siirrytään suoraan tekemään tietokantahakua, niin nappi jää klikatessa alas ja käyttöliittymä lakaa toimimasta kunnes haku on suoritettu (ilmiö näkyy vain tarpeeksi raskaalla haulla). Tämä johtuu siitä että Swing tekee kaiken yhdessä säikeessä (ns. tapahtumankäsittelijäsäie).

Kaikki ei-triviaali täytyy siis saada toiseen säikeeseen, mutta Swing ei ole säieturvallinen. Harvoja poikkeuksia lukuunottamatta mitään Swing komponentin metodia ei saisi kutsua muusta kuin tapahtumankäsittelijä säikeestä. Esimerkiksi niinkin yksinkertainen toimenpide kuin taulukon rivimäärän muuttaminen toisessa säikeessä voi jumiuttaa käyttöliittymän (mikä ikävä kyllä, ei ilmene välitömästi bugina).

Swingin ja säikeiden käyttö tulee äkkiä ongelmalliseksi ja hyvien koodaustapojen ja suunittelumallien käyttäminen välttämättömäksi.

Model-View-Controller mallissa model vastaa tiedon käsittelystä ja talletuksesta, controller vastaa ohjelman tilasta ja käyttäytymisestä, ja view vastaa tilan ja tiedon näyttämisestä. Jako on lähinnä filosofinen ja hämärtyy helposti varsinkin graafisessa käyttöliittymässä ja graafisia käyttöliittymän luonti työkaluja käyttäessä.

Yksi näkemys MVC suunittelumallista

Mielestäni Swing ja kaikki sen osia suoraan käyttävä kuuluu viewin osaksi. Osittain sen takia, että silloin olisi ainakin teoriassa helppo tehdä muutoksia käyttöliittymään, mutta pääasiassa siksi, että Swingiä käyttävien osien tiukka rajaaminen erilleen helpottaa useiden säikeiden käyttöä. Kun Swing-käyttöliittymän vastuu on rajattu vain tapahtumien aloittajaksi ja tilan muutosten kuuntelijaksi on tapahtumankäsittelijäsäikeellä varmasti riittävästi aikaa käyttäjän syötteisiin reagointiin ja käyttöliittymän uudelleenpiirtämiseen.

Command -suunittelumalli ja Swingin Action -oliot

Command suunittelumalli helpottaa Swing käyttöliittymän (view) ja controllerin erossa pitämistä toisistaan. Lyhyesti sanottuna command -mallissa käyttöliittymän komponentit eivät kutsu controllerin metodeja suoraan vaan välillisesti Action -olioiden kautta. Action -oliot piilottavat käyttöliittymältä sen miten contolleria käytetään ja toimivat samalla mahdollisena kohtana luovuttaa toiminnon suoritus toiselle säikeelle.

Esimerkiksi napin painallus, joka aloittaa raskaan tietokanta haun, tulisi erotella seuraavasti.
1. Periytetään Action -luokasta olio, jossa execute -metodissa sopivalle worker -säikeellä annetaan tehtäväksi ko. tietokantahaku.
2. Tehdään käyttöliittymään nappi ja aseteaan sille ActionListeneriksi edellä tehty Action -olio.

Kun nappia klikataan tapahtumankäsittelijä säie kutsuu napin action olion execute -metodia, joka aloittaa tietokantahaun tekevän säikeen suorituksen. Tämän jälkeen tapahtumankäsittelijä säie on heti vapaa suorittamaan käyttöliittymän uudelleenpiirtoa yms. Kun käyttöliittymään halutaan sama toiminto esim. nappiin, popup menuun ja ikkunan päävalikkoon voidaan kaikkiin asettaa ActioninListeneriksi sama Action -olio. Toiston minimoimisen lisäksi tästä on se hyöty, että toiminnon disabloimisen voi tehdä disabloimalla sen suorittava Action -olio, mikä disabloi samalla kertaa kaikki komponentit joiden ActionListener se on.

Esimerkki Actionin ja säikeiden käytöstä:

startHardDataSearchButton.addActionListener( new HardDataSearchAction());

Käyttöliittymäkoodissa asetetaan button oliolle vain action, jonka painalluksen halutaan suorittavan.

class HardDataSearchAction extends AbstractAction{ public void actionPerformed(ActionEvent ae) { //Huom. _oikeasti_ käytettäisiin //jotakin ThreadpoolExecutor //luokkaa, eikä ad-hoc luotua säiettä. new Thread(new HardDataSearchRunner()).start(); } }

Action -luokassa siirretään suoritus jollekin muulle säikeelle, minkä jälkeen tapahtumankäsittelijäsäie on vapaa suorittamaan käyttöliittymän uudelleenpiirtoa ja vastaanottamaan käyttäjän syötteitä.

class HardDataSearchRunner implements Runnable{ public void run(){ /*Tietokantakysely tai vastaava raskas operaatio suoritetaan Runnablen run metodissa ja suorituksen valmistumisesta ilmoitetaan käyttöliittymälle tapahtumankäsittelijä säikeessä. */ final List<ResultObject> resultOfHardSearch = .... SwingUtils.invokeLater( new Runnable(){ public void run{ //tämä koodi suoritetaan //tapahtumankäsittelijä säikeessä mySwingGui.setSearchResult(resultOfHardSearch); }}); } }

Observer suunittelumalli

Edelläoleva esimerkki on esimerkeille tyypilliseen tapaan koodipainotteinen ja sellaisenaan epäsopiva oikeaan käyttöön. HardDataSearchRunner ja sitä kautta koko Action -olion toteutus sitoo käyttöliittymän muihin ohjelman osiin molempiin suuntiin (käyttöliittymä -> action -> tietokantakysely -> käyttöliittymän päivitys). Tämä luuppi voidaan katkaista Observer -suunnittelumallilla.

Observer mallissa olio (Observable), jonka tilaa halutaan seurata, ilmoittaa tilan muutoksistaan sellaisiille olioille (Obeserver), jotka ovat rekisteröityneet sen tilan seuraajiksi. Observable -olio ei tiedä, eikä sen kuulukkaan tietää, miksi Observer -oliot ovat kiinnostuneita sen tilasta.

Edellä olevaan esimerkkiin sovellettuna Observer -mallia voitaisiin käyttää siten, että resultOfHardSearch talletetaan osaksi ohjelman ajonaikaista tilaa sopivaan osaan controlleria.

class SearchController{ private List<ResultObject> currentResults = new ArrayList<ResultObject>(); public Observable Observable= new Observable(); public void doHardSeach(final String seachKeyword){ MyApplication.getMyThreadPoolExecutor.execute( new Runnable(){ public void run(){ executeSearc(seachKeyword); } } } private void executeSearc(String seachKeyword){ currentResults=database.someSearch(seachKeyword); resultObservable.notifyObservers(); } public List<ResultObject> getResults(){ return new ArrayList<ResultObject>(currentResults); } }

Esimerkin muuttaminen käyttämään Observer -mallia menisi seuraavasti: HardDataSearchActionin actionPerformed kutsuu SearchConrollerin doHardSeach metodia, joka käyttää executor luokkaa varsinaisen haun tekemiseen. Käyttöliittymän komponentti joka näyttää hakutulokset rekisteröi itsensä SearchController:in Observeriksi (SearchController.observable.addObserver()) ja saa ilmoituksen kun hakutulokset on suoritettu loppuun (rivi resultObservable.notifyObservers()). Muutosilmoituksen saava käyttöliittymä komponentti hakee kopion hakutuloksista metodilla getResults() ja korvaa sillä oman listansa tapahtumankäsittelijäsäikeessä samaan tapaan kuin edellä HardDataSearchRunner.