|
Java Tutorial - Parte 1 0.1
Un tutorial per esempi
|
In questo capitolo l'autore vi insegnerà alcuni tips and tricks (=consigli e trucchetti) sviluppati nel corso della sua esperienza. Non si tratta affatto di regole da seguire assolutamente ed, anzi, il lettore è spronato a sviluppare le proprie tecniche di ottimizzazione: questa vuole solo essere una traccia per cominciare.
Nella documentazione della classe (vedi CrivelloNative è stato inserito questo avvertimento:
close di questa classe che libera le risorse allocate. L'avvertimento appare chiaro: nel linguaggio "C" le zone di memoria allocate devono obbligatoriamente essere liberate ma poichè nel design della classe Crivello abbiamo deciso di lasciare allocato il setaccio per usi futuri, non abbiamo alcuna altra possibilità per rilasciare la memoria se non quella di richiamare esplicitamente il metodo CrivelloNative.close() il quale a sua volta richiamerà la funzione nativa closeJNI o closeFFM a seconda della libreria nativa usata.
Eseguire questa operazione non è difficile: basta inserire il relativo codice nel main ma dove dobbiamo inserirlo? Ebbene, l'unico posto sicuro dove farlo è alla fine del main dopo le eventuali catch delle eccezioni:
Tuttavia, questo non è un buon approccio. Non si dovrebbe mai lasciare questo compito al programmatore nè a quello esterno (colui che usa la nostra classe) nè a noi stessi giacchè una dimenticanza può sempre capitare.
Ma cosa succede se non libero la memoria allocata per il setaccio nella libreria nativa? Ebbene, essa rimarrà allocata e inutilizzabile: il sistema operativo la vede assegnata ad un processo e non può riassegnarla finchè non liberata; in gergo informatico questa situazione viene chiamata memory leak (=perdita di memoria).
Se il metodo getPrimesCount viene nuovamente richiamato da una nuova istanza della classe algoritmo, il setaccio sarà nuovamente allocato e, se ancora non viene liberato una terza volta, una quarta volta e così via ci troveremo con la memoria piena di memory leaks fino a quando la memoria stessa non sarà completamente satura.
Vi è comunque da osservare che per quanto riguarda la nostra piccola applicazione Performance la saturazione della memoria non accadrà mai: il nostro programma ha un inizio ed una fine e nei moderni sistemi operativi quando un processo termina, tutte le risorse usate (memoria allocata, flussi di I/O aperti) vengono regolarmente ed automaticamente rilasciate (la memoria deallocata, i flussi di I/O chiusi).
Tuttavia, è buona norma di programmazione evitare al massimo di dipendere dal sistema operativo per il cleanup delle risorse.
Come già accennato, il linguaggio "C" non consente alcun approccio automatico al rilascio delle risorse ma il suo fratello maggiore, il "C++", essendo un linguaggio orientato agli oggetti, lo consente.
In "C++" ogni oggetto viene creato attraverso il suo costruttore (esatto, proprio come in Java) ma, quando esso non serve più, il compilatore richiama
la controparte del costruttore che, come logico sia, si chiama distruttore. E' nel distruttore che il programmatore "C++" inserisce il codice di cleanup e cioè, nel nostro caso, il richiamo a CrivelloNative.close()
Ma questo non basta: per essere sicuri che il distruttore venga eseguito automaticamente, l'oggetto deve essere creato sullo stack e non nello heap. Se osservate questo spezzone di codice (lo so, è uguale a Java) capirete di cosa sto parlando:
Nel linguaggio "C++" è abitudine comune tra i programmatori tenere il metodo main al minimo indispensabile; la tecnica più usata è quella di:
Application con un suo metodo run che, a tutti gli effetti, corrisponde al main Application che esegue il cleanup run In questo modo, non ha alcuna importanza se la applicazione termina normalmente o con delle eccezioni: il distruttore sarà sempre ed in ogni caso eseguito automaticamente.
Java è un linguaggio orientato agli oggetti, al pari di "C++", ma l'approccio di quest'ultimo non può applicarsi a Java per due motivi:
new.Tuttavia, Java mette a disposizione del porgrammatore uno strumento potente e flessibile che simula in tutto e per tutto l'approccio "C++" per creare una applicazione "che si chiude da sola", liberando le risorse allocate nelle librerie native. Per ottenere questo, procedete come segue:
main main nel metodo run close() che contiene il richiamo a CrivelloNative.close() Ora potete creare il nuovo metodo main dal quale non avete alcun bisogno di richiamare esplicitamente il metodo close della classe Performance2: esso sarà richiamato automaticamente, sia se la app si chiude normalmente che in caso di eccezioni. Questo è il nuovo main:
Il costrutto che ho usato per ottenere questo risultato è noto come il "blocco try-with-resources" (=tenta con le risorse) la cui sintassi generale è la seguente:
In elenco_delle_risorse vanno inserite le istruzioni che creano istanze di oggetti che il compilatore Java considera risorse e che devono esswre chiuse regolarmente. Dette risorse possono essere di qualsiasi tipo: l'elenco classico di risorse da inserire in un blocco try-with-resources sono i flussi di input/output ma, come potete osservare, anche una classe applicazione può essere considerata una risorsa.
Ciò che rende una qualsiasi classe una risorsa sono due sole caratteristiche:
AutoCloseable public void close() Questo approccio alla programmazione Java vi consente di scrivere applicazioni "che si chiudono da sole". Se il metodo close è progettato bene, avete la sicurezza assoluta che le risorse allocate sono state regolarmente rilasciate. E' un buon modo per cominciare a scrivere programmi CLI (Command Line Interface = programmi a riga di comando); le applicazione GUI (Graphic User Interface = programmi ad interfaccia grafica) sono tutta una altra faccenda e questo trucchetto non funziona. La programmazione GUI sarà affrontata nella seconda parte di questo mio tutorial su Java.
Avrete sicuramente notato alcuni strani messaggi nell'eseguire il programma Performance usando algoritmi che richiamano funzioni delle librerie native:
DLL DEBUG: setacciaFFM() - limit=100000000 DLL DEBUG: setacciaFFM() - size_t size=8 DLL DEBUG: setacciaFFM() - long long size=8 DLL DEBUG: setacciaFFM() size of setaccio is: 6250000 bytes DLL DEBUG: closeFFM() DLL DEBUG: Allocating 6250004 bytes for setaccio DLL DEBUG: Limit of setaccio: limit=100000063, size=6250004 DLL DEBUG: countFFM() - limit=100000063
Appare ovvio che provengono dalle funzioni delle librerie native ma la domanda è: perchè dobbiamo inquinare l'output del programma con messaggi che non hanno alcun senso per l'utente? Avete ragione: questi messaggi non hanno alcun senso per l'utente ma ne hanno eccome per il programmatore! Ricordate? E' proprio grazie a questi messaggi che abbiamo scoperto il bug in Il crash della applicazione.
Nei linguaggi compilati è uso comune tra i programmatori inserire i messaggi cosidetti di debug (=per eliminare gli errori) nel codice. I linguaggi compilati hanno una fase che si chiama preprocessing (vedi Il build di un eseguibile) in cui è possibile, attraverso la direttiva del preprocessore #define sostituire un simbolo ad una qualsiasi sequenza di caratteri. Per esempio:
Ma è la stessa cosa delle costanti mnemoniche di Java, direte voi (vedi I membri dati statici). Avete (quasi) ragione. Uno degli utilizzi della direttiva define in "C/C++" è effettivamente quello di definire le costanti mnemoniche ma il modo in cui avviene è del tutto diverso: mentre in Java si definiscono delle vere e proprie variabili (quindi celle di memoria che occupano spazio) ma che contengono valori statici e finali, nei linguaggi compilati avviene una vera e propria SOSTITUZIONE a livello del file sorgente.
Questo consente di usare la direttiva in modi che non vi aspettereste nemmeno, e che in Java non si possono usare. Esempio:
Per mezzo della sostituzione, il simbolo CIAO si espande nel main nella funzione di libreria standard del "C" printf, la quale visualizza a terminale il messaggio fornito come argomento. Per avere i messaggi di debug nella libreria nativa si definisce il simbolo DEBUG che si espande proprio nella funzione printf:
Il trucco per avere o meno i messaggi di debug è quello di prevedere un altro simbolo, tra i programmatori "C/C++" si usa convenzionalmente il simbolo NDEBUG (=no debug), il quale:
Pertanto, se volete compilare le librerie native senza i messaggi di debug non dovete fare altro che ricompilare i sorgenti, avendo cura di specificare sulla riga di comando il simbolo NDEBUG:
>gcc -O3 -fPIC -shared -static-libgcc -DNDEBUG -o setaccioffm.dll setaccioffm.c
Il linguaggio Java non possiede una fase di preprocessing e quindi la espansione della direttiva define non esiste come non esiste nemmeno una direttiva define. Tuttavia, Java mette a disposizione del programmatore una utility di gran lunga più potente e flessibile: il logging, che è contenuto nel package java.util.logging.
In questa sezione il lettore imparerà solo i rudimenti del logger; questo argomento verrà descritto in dettaglio nella seconda parte di questo tutorial su Java, quando vi verrà insegnata la programmazione GUI.
Il nome logger deriva dal verbo inglese to log che letteralmente significa "registrare" ma viene usato normalmente come parola composta per esempio in logbook che significa "diario di bordo" oppure anche daylog che può essere tradotto in "diario giornaliero".
Un logger si crea richiamando il metodo statico Logger.getLogger che ritorna un oggetto logger con il nome specificato nel suo argomento. Questo oggetto deve rimanere vivo se intendete usarlo in tutte le classi: è il nome del logger che stabilisce l'oggetto ritornato dal metodo getLogger ma il reference deve essere attivo, nel senso che, se il logger è stato collezionato nel garbage esso potrebbe non esistere più e un altro viene creato.
Per questo motivo il logger viene creato di solito nel main e memorizzato in una variabile statica.
In secondo luogo, un logger possiede uno o più handlers (=gestori): è possibile infatti inviare i messaggi di log a diversi dispositivi: il terminale, un file di disco e perfino una connessione remota. Di default, il logger viene creato con un handler di tipo ConsoleHandler che invia i messaggi di log al terminale. Se intendete utilizzare un handler specifico dovete aggiungerlo al logger usando il metodo addHandler.
In terzo luogo, gli handlers vengono usati dal logger come una catena nella quale ogni handler riceve il messaggio di LOG. Si può evitare che ciò avvenga limitando il LOG al solo handler aggiunto più di recente usando il metodo setUseParentHandlers:
Di seguito un esempio di come si è creato il logger per la nostra applicazione:
I messaggi di log possono essere davvero tanti, persino troppi. Supponiamo di inserire un messaggio di log nel metodo isCancelled del crivello: questo metodo viene eseguito per ogni numero setacciato e, quindi, se il limite del setaccio è un-milione avremmo un-milione di messaggi di log.
Ma non è quello che vogliamo: è interessante sapere che il setaccio è stato allocato e a quale limite ma non ci interessa il log di ogni singolo numero; o meglio, ci interessa se il limite è piccolo, per verificare il comportamento del codice che abbiamo scritto.
Il logger di Java possiede quindi il concetto di livello di log che viene impostato nel logger: solo i messaggi di log che hanno un livello più elevato vengono effettivamente inviati agli handlers. I livelli di log sono valori numerici ma la classe Level definisce costanti mnemoniche per facilitare il lavoro:
Gli stessi valori possono essere specificati come una stringa che, ovviamnete, ha lo stesso significato. La classe Level mette a disposizione il metodo statico parse per interpretare una stringa come un livello di log:
Per usare il logger è necessario ottenerne un riferimento in ogni classe i cui metodi intendiamo loggare. Di solito, il logger si ottiene nel costruttore e si memorizza in un membro dati statico protetto in modo da averlo a disposizione anche nelle classi derivate, senza necessità di ottenerlo nuovamente:
Ottenuto l'oggetto logger possiamo utilizzarlo in ogni metodo il cui andamento vogliamo registrare. Per emettere un messaggio di log si deve richiamare il metodo log dell'oggetto logger seguito da due argomenti:
Vi sono molte altre versioni sovraccaricate del metodo log le quali possono accettare altri argomenti. Per maggiori informazioni vedi java.util.logging.Logger
la stessa classe Logger mette a disposizione versioni di comodo del metodo log il cui nome è uguale a quello del livello ma in caratteri minuscoli e che hanno un significato davvero esplicito:
Nella applicazione Performance con algoritmo C3 (=Crivello3) il setaccio viene allocato su base bit e per i soli numeri dispari e quindi il limite del setaccio è sempre un multiplo di 16 meno uno. Il metodo setaccia viene eseguito con un argomento: il numero più grande da setacciare. Il metodo ritorna il nuovo limite del setaccio che è sempre uguale o maggiore del numero più grande da setacciare.
Sarebbe molto interessante loggare queste due informazioni:
Per esempio, questo è l'output del programma specificando come numero il 100.000; osservate che il setaccio ha un limite di 100.015 numeri; noi non sprechiamo nulla della preziosa memoria del computer, nemmeno un bit:
INFO: Crivello3.setaccia() - limit = 100000 INFO: Crivello3.setaccia() - actual limit = 100015
Come già accennato in precedenza, questi messaggi informativi, assolutamente utili al programmatore, sono inutili per l'utente anzi, direi addirittura fastidiosi. Cosa cavolo significa "Crivello3.setaccia() - limit = 100015"?
In effetti, la buona abitudine di programmazione deve avere una strategia ben precisa che aiuti il programmatore a risolvere i bugs ma che non causi fastidi agli utenti.
Questa strategia si basa su una ben nota abitudine: la applicazione deve essere rilasciata con l'output che l'utente si aspetta e non inquinato di messaggi criptici; se tutto va per il verso giusto, non ci sono problemi ma se il programma comincia a comportarsi in modo anomalo o dare risultati non corretti, il programmatore chiederà all'utente di eseguire il programma con gli stessi argomenti che hanno causato la anomalia avendo cura di specificare una opzione speciale che, per il programma Performance è la -o seguita dal livello di log:
L'output conterrà i messaggi di log che l'user invierà al programmatore.
Rimane un problema da risolvere: la libreria nativa non è a conoscenza della utility di logging di Java e, daltronde, nemmeno potrebbe: non è stata scritta in Java. Abbiamo visto in Il debug nei linguaggi compilati che è possibile ottenere una versione contenente i messaggi di debug ricompilando da libreria nativa ma questa è una operazione che non può certo essere lasciata al user: persino il programmatore Java potrebbe avere difficoltà a farlo figuriamoci l'utente.
Ma una soluzione esiste: è abitudine dei programmatori "C/C++" rilasciare sempre due versioni della libreria nativa:
Normalmente, le due versioni hanno lo stesso nome ma a quella di debug viene aggiunto il suffisso _d nel nome del file. Anche nel package javatutor.primes.crivello esistono due files distinti per la libreria nativa:
setaccioffm.dll la versione release setaccioffm_d.dll la versione debug Sono entrambe versioni binarie, create dal programmatore "C", e quindi non hanno alcun impatto sul progrmmatore Java al quale, di solito, non vengono forniti i sorgenti, almeno non usando la API FFM, ricordate (vedi JNI ed i sorgenti "C")?
Il trucco, usando la API FFM, è quello di caricare entrambe le libreria native nella JVM:
ed usare un argomento al costruttore della classe CrivelloFFM per specificare se deve essere usata la libreria in versione release o in versione debug. Per ottenere questo, basta un boolean (per esempio true per usare la versione di debug.
Nel costruttore possiamo quindi analizzare il valore dell'argomento per caricare in modo selettivo la symbol table (=tabella dei simboli) corretta:
L'argomento boolean al costruttore sarà ovviamente impostato nel metodo principale della applicazione e, nello specifico, nel metodo setLogLevel che abbiamo già visto: se in quel metodo il livello di log è diverso da Level.OFF, allora si attiverà il flag da passare al costruttore della classe CrivelloFFM.
Con questo trucchetto, quando l'user specificherà la opzione:
-oINFO
sulla command-line l'output conterrà sia i messaggi di logging propri di Java sia i messaggi de debug della libreria nativa. E tutto questo senza che nè l'user nè il programmatore Java debbano avere contezza del linguaggio "C" col quale è stata realizzata la libreria nativa.
Nella cartella javatutor1/projects/primes.v2 troverete i sorgenti della applicazione Performance con le modifiche descritte in questo breve capitolo sui consigli e trucchetti. La documentazione completa in formato javadoc di questo progetto è disponibile al seguente link: Il progetto ListPrimes v2