|
Java Tutorial - Parte 2 0.1
Un tutorial per esempi
|
In questo capitolo il lettore impererà ad usare i threads secondari per le elaborazioni time-consuming. Ho più volte accennato al fatto che una applicazione GUI viene eseguita in un thread principale chiamatgo EDT (Event Dispatching Thread); vedi a questo proposito la sezione Che significa invokeLater?.
I metodi che rispondono agli eventi devono durare pochissimo tempo, il minimo indispensabile e rientrare subito: in caso contrario la GUI si comporta in maniera inaspettata come abbiamo sperimentato in Event Dispatching Thread.
In informatica un thread viene spesso chiamato processo leggero. La differenza sostanziale tra un processo ed un thread sta nel fatto che i processi in esecuzione su un computer sono isolati e nettamente separati gli uni dagli altri. Ogni processo ha il suo spazio di indirizzamento privato in memoria e nessun processo può accedere allo spazio di un altro processo: se lo fà il sistema operativo lo termina immediatamente.
I threads, invece, condividono lo spazio degli indirizzi del processo a cui appartengono e, pertanto, ogni thread ha accesso ai membri dati ed ai metodi definiti nella applicazione: un ulteriore vantaggio dei threads rispetto ai processi è che i primi sono facili e veloci da creare e mandare in esecuzione.
Tuttavia, non sempre le elaborazioni da eseguire in risposta agli eventi possono concludersi immediatamente: in questo giochino la AI deve elaborare un numero limitato di possibili soluzioni ma come la mettiamo se dovessimo scrivere una AI per il gioco degli scacchi?
Le elaborazioni che concumano tempo possono essere eseguite da applicazione GUI (ci mancherebbe altro) ma non nel EDT; faremo partire un thread a sè stante, che chiameremo secondario, che può durare tutto il tempo necessario: quando esso si sarà concluso, i risultati saranno "notificati" al EDT che li visualizzerà nei componenti GUI della app.
Come facciamo a "rallentare" l'algoritmo classico della AI in modo che ci debba "pensare un pò sopra" prima di fornire la guess? Ma è facile: introdurremo un ritardo fittizio che specificheremo nel costruttore della classe PlayerAI. Il valore passato come argomento, specificato in millisecondi, rappresenta il ritardo che il giocatore deve aspettare prima di inviare la sequenza di guess al server di gioco.
Per mantenere la coerenza con la strategia di versioning adottata finora, scriveremo una nuova classe, derivate da quelle originale, per la intelligneza artificiale. Di seguito l'elenco dei nuovi files sorgente:
| nome file | package | descrizione |
|---|---|---|
| PlayerAI06.java | mastermind.player | la nuova intelligenza artificiale |
| Main06.java | mastermind.app | la classe principale della versione 0.6 |
Introdurre un ritardo in un processo è facilissimo; il linguaggio Java fornisce nella sua libreria standard il metodo statico Thread.sleep(milliseconds) che fa dormire il thread corrente per il numero specificato di millisecondi, La granularità del timer dipende dalla piattaforma specifica. Java mette a disposizione del programmatore diversi metodi e classi per gestire il tempo, vedi Gli orologi del sistema.
Potremmo semplicemente sovrascrivere il metodo della classe PlayerLocal introducendo il ritardo fittizio facendo dormire il thread corrente:
Il metodo statico Thread.sleep solleva la eccezione checked InterruptedException quando il thread viene interrotto, solitamente perchè il processo stesso in cui gira il thread viene interrotto o finisce. Non c'è molto dare fare se viene sollevata questa eccezione dal momento che il thread sta per terminare; la eccezione viene semplicemente ignorata.
Di quanti millisecondi facciamo dormire il thread? La soluzione migliore è quella di lasciare questo valore impostabile a piacimento e quindi lo specificheremo come argomento al costruttore della classe. Non modifichiamo il costruttore già in essere: se lo facessimo, romperemo la compatibilità con le versioni precedenti e le classi Main04 e Main05 non compilerebbero più. La strategia corretta in questi casi è proprio quella di scrivere un nuovo costruttore:
D'ora in poi, questa è la classe che dobbiamo usare nel metodo createPlayer della applicazione principale. E se ci dimentichiamo di farlo e usassimo ancora la vecchia classe PlayerAI che manteniamo nei sorgenti per compatibilità con le versioni precedenti? Il rischio di usarla ancora la vecchia classe per sbaglio è piuttosto alto.
Il linguaggio Java ci viene in aiuto per mitigare questo rischio con le annotations (=annotazioni) e, nello specifico la annotazione @Deprecated che scriviamo subito prima della classe il cui uso è "sconsigliato".
La annotazione fà parte del compilatore Java il quale ci avverte, in fase di compilazione, che abbiamo usato una classe, un metodo e/o un costruttore "deprecato" (=sconsigliato). Se proviamo a compilare il sorgente Main05.java, la versione precedente della applicazione principale che fà ancora uso del vecchio costruttore, otterremo un avvertimento:
mastermind\V060>javac mastermind\game\Main05.java Note: mastermind\game\Main05.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details.
Specificando la opzione -Xlint:deprecation il compilatore ci informa dei dettagli e cioè quali istruzioni usano la API deprecata:
mastermind\game\Main05.java:94: warning: [deprecation] PlayerAI in mastermind.player has been deprecated
case "AI" -> new PlayerAI( turn );Il messaggio del compilatore non è un errore ma un warning (=avvertimento) ed il sorgente compila correttamente. E' compito del programmatore analizzare il warning: sappiamo che le vecchie versione della classe principale usano la vecchia classe PlayerAI ma non è un problema.
Diverso il caso se il warning riguardasse sorgenti nuovi: in questo caso dobbiamo correre ai ripari e la documentazione javadoc ci viene in aiuto mettendo a disposizione due speciali comandi:
@deprecated che ha lo stesso significato della annotazione del compilatore ma al quale possiamo aggiungere del testo a nostro piacere @since mediante il quale si è solito indicare il numero di versione dal quale una classe, un metodo e/o costruttore è disponibileLa nuova classe Main06 deriva da Main05 in modo da poterne ereditare tutte le funzionalità già implementate nella versione precedente e sovrascrive, oltre al costruttore ed al metodo getVersion anche il metodo connectPlayers il quale usa il nuovo costruttore di PlayerLocal e, di conseguenza, anche delle sue classi derivate:
Il valore del ritardo fittizio in millisecondi viene specificato dalla proprietà "guessDelay" che per default è impostata a 1000 (un secondo) Possiamo però specificare un valore diverso sulla cmdline:
>java -ea mastermind.game.Main guessDelay=0
Se provassimo questo nuovo feature con il codice descritto in Il metodo 'sleep' in cui abbiamo semplicemente introdotto il ritardo con una semplice istruzione sleep (ed il relativo blocco try..catch) osserviamo che, effettivamente, la guess del giocatore AI non viene visualizzata subito dopo che noi abbiamo premuto il bottone Check ma con un certo ritardo che possiamo valutare in circa un secondo come da noi desiderato.
Tuttavia, c'è qualcosa che non quadra: continuamo la partita, inserendo altre guess senza pensarci troppo e ad un certo punto notiamo le stranezze:
Cosa è successo? Ebbene, la risposta è: la GUI non si aggiorna!
Ho già accennato al fatto che una applicazione GUI Java viene eseguita in due threads diversi:
main L'aggiornamento della GUI viene gestito come evento dal EDT nel dispatcher (il gestore degli eventi) e fintanto che esso non viene eseguito non si può avere alcun aggiornamento visivo.
Mi spiego meglio con un esempio. Prendiamo come esempio l'aggiornamento della statusbar che avviene nel metodo notifySwapTurn:
Il nuovo turno viene impostato correttamente nel oggetto StatusBar il quale a sua volta imposta il colore di fondo (verde) nella JLabel relativa al turno indicato nell'argomento:
Il codice è assolutamente corretto. Ma allora, perchè non lo vediamo? Ebbene, non si vede nulla perchè il metodo della libreria Swing JLabel.setBackground NON disegna affatto il nuovo colore di fondo nel componente ma si limita a impostare il nuovo colore di fondo nei membri dati del componente e poi "accoda" un evento PaintEvent nella coda degli eventi.
L'evento accodato contiene tutte le informazioni su quale componente deve essere aggiornato in visualizzazione. Come tutti gli eventi, esso viene accodato nella coda ed elaborato dal dispatcher quando il controllo ritorna al dispatcher stesso.. Tuttavia, il controllo non ritorna subito al dispatcher poichè il EDT è bloccato in un Thread.sleep e questo impedisce al dispatcher degli eventi di eseguirne la coda. Solo quando il EDT si "risveglia" il dispatcher viene eseguito di nuovo.
In una applicazione GUI non si deve MAI eseguire elaborazioni costose in termini di tempo nel EDT altrimenti la GUI diventa unresponsive (=non responsiva) cioè non esegue la coda degli eventi e pertanto sembra non rispondere più ai comandi. Come si risolve questo problema? Semplice: le elaborazione che consumano tempo si eseguono in un thread separato.
Per creare nuovi threads il linguaggio Java fornisce la classe Thread la quale implementa la interfaccia Runnable il cui unico metodo astratto è il run che non accetta argomenti e ritorna void. La creazione e gestione di un thread mediante la classe all'uopo predisposta è piuttosto complicata e questo non è il momento adatto per affrontare aspetti così complessi della programmazione.
Per fortuna, il linguaggio Java mette a disposizione uno strumento facile da usare e veloce da implementare ed è stato scritto in modo specifico per essere usato in una applicazione Swing. Quando si scrive una applicazione multi-thread in Swing (e, di norma, anche in altri ambienti GUI) si devono osservare alcune restrizioni:
Appare quindi chiaro che scrivere un thread separato non è banale nemmeno per introdurre un semplice ritardo di un secondo nel metodo che invia una guess da parte del giocatore locale poichè è necessario:
Si deve quindi in qualche modo implementare una sorta di comunicazione tra il thread separato che fornisce i risultati e l'EDT che li riceve e li usa per aggiornare la GUI accedendo ai componenti grafici.
La classe SwingWorker del package javax.swing facilita enormemente questo lavoro consentendo al programmatore di disinteressarsi del tutto di questi aspetti tecnici: tutto quello che serve sapere al programmatore sono pochi concetti fondamentali:
SwingWorker può essere costruito anche mediante una classe anonima poichè il codice da scrivere è composto da poche righe execute SwingWorker non può più essere ri-eseguito: è necessario creare una nuova istanza di esso e richiamare il metodo execute Per eseguire il lavoro nel worker è sufficente sovrascrivere due soli metodi:
doInBackground: questo metodo viene eseguito nel thread secondario e può pertanto essere lungo a piacere; esso ritorna il risultato della elaborazione done; questo metodo viene eseguito nel EDT e quindi è possibile accedere a qualsiasi componente GUI in questo metodo. Per ottenere i risultati elaborati in background si usa il metodo get.Quindi nella classe PlayerAI06 sovrascriviamo il metodo sendGuess della classe base PlayerLocal ed implementiamo uno SwingWorker che fà partire un thread secondario che dura il tempo necessario ad introdurre il ritardo fittizio. Quando il thread secondario termina, la guess viene restituita al EDT tramite il metodo done il quale poi la invia al server di gioco per la ulteriore elaborazione. Osservate quando facile è creare ed eseguire qualsiasi compito, anche molto lungo, in un thread secondario usando Java Swing:
Nel metodo eseguito in background si inserisce il ritardo facendo dormire il thread secondario: poichè si tratta di un thread separato, il EDT continua ad eseguire e la GUI non si blocca: il thread secondario termina ritornando la guess elaborata dalla AI.
Quando il thread secondario termina, dal EDT viene richiamato il metodo done il quale recupera il dato elaborato in background richiamando il metodo get. Il dato recuperato, cioè la guess già elaborata, viene inviata al server di gioco richiamando il metodo haveGuess della interfaccia InputListener.
Il thread separato viene creato istanziando una classe anonima di tipo SwingWorker che è classe parametrizzata. La sua definizione è la seguente:
Poichè i metodi della nostra classe anomina devono restituire String quale risultato della elaborazione in background (la guess, peraltro già conosciuta) e nessun risultato intermedio, istanziamo la classe anomina con:
Il "lavoro" viene eseguito sovrascrivendo i due metodi all'uopo predisposti della classe SwingWorker:
doInBackground che è il metodo eseguito nel thread separato e che restituisce il dato elaborato done che viene eseguito nel EDT ed ottiene il dato elaborato in background Questo è davvero facile:
La elaborazione in background introduce il ritardo stabilito nel membro dati delay il cui valore viene passato al costruttore di PlayerLocal. Allo scadere, ritorna la sequenza di guess passata come argomento al metodo. Il metodo statico Thread.sleep può sollevare eccezioni di tipo InterruptedException che sono di tipo checked e quindi vanno obbligatoriamente intercettate o propagate.
Il metodo done viene eseguito nel EDT; otteniamo la guess con il metodo get e la inoltriamo al server di gioco per mezzo del metodo di interfaccia haveGuess.
Un commento speciale meritano le eccezioni: il metodo get deve essre eseguito in un blocco try ... catch perchè può sollevare due eccezioni checked:
InterrupedException: sollevata se il thread separato è stato interrotto; questa eccezione di solito viene ignorata poichè se l'elaborazione in background è stata interrotta allora la applicazione non è interessata ai risultati ExecutionException: se il metodo doInBackground ha sollevato una qualsiasi altra eccezione; in questo caso la eccezione sollevata dal metodo in background viene impostata come causa della eccezione ExecutionException Nel nostro caso, il metodo doInBackground non può sollevare eccezioni diverse da InterruptedException considerato che la sequenza di guess da ritornare è disponibile già in ingresso al metodo haveGuessDelayWorker; essa deve solo essere ritornata allo scadere del ritardo.
La classe Main06, derivata da Main05, è davvero facile; essa si limita a sovraascrivere il metodo createPlayer il quale da questa versione in avanti istanzia la nuova classe PlayerAI06 per il giocatore A.I.
Compiliamo e proviamo: la GUI è responsiva, il flag del turno viene aggiornato e si nota anche che i bottoni di comando usati dal giocatore umano per digitare la guess sono disabilitati nel turno della AI.
PlayerAI06.java esistono due versioni del metodo sendGuess: SwingWorker, e che consente alla app di comportarsi normalmente sendGuess e togliete i commenti alla versione alternativa. Compilate ed eseguite: noterete immediatamente i problemi che ho descritto. La documentazione completa del package descritto in questo capitolo può essere visualizzata clikkando il seguente link che riporta alla documentazione Javadoc della versione 0.6 del progetto: The MasterMind Project Version 0.6
Argomento precedente - Argomento successivo - Indice Generale