Sinistra <- Preseeding Debian Parte I - Indice Generale - Copertina - PLUTO Meeting 2007 -> Destra

PLUTOWare


I thread in Perl: un programma di chat

Un programma di chat: ritorno dall'ignoto
I thread in Perl, standard aperti ed interoperabilità, diario di un esploratore.

di Marco Marongiu, Stefano Sanna

Traduzione di Pietro Leone

L'articolo...

Come imparare alcune cose in Perl...divertendosi (articolo comparso in lingua inglese sul Free Software Magazine)



Alcune settimane fa io, Marco, stavo cercando nuove cose da imparare in Perl. Ho dato un'occhiata alla mia biblioteca, ho scorso i titoli dei libri che ho letto di meno e, dopo alcune considerazioni, ho trovato due argomenti: interfacce grafiche e thread, ma, poiché odio i programmi tipo "Ciao, mondo", ho deciso di iniziare l'esplorazione di questi aspetti sconosciuti (per me) del Perl con qualche cosa di più interessante: una chat.

Il progetto

Quando faccio questo genere di cose, mi piace discutere delle mie idee con altre persone ed è a questo punto che entra sempre in gioco Stefano. Lui è un programmatore in Java e conosce molto bene come gestire le interfacce ed i thread. Abbiamo discusso del progetto e mi ha dato alcuni suggerimenti sulla struttura del programma. Abbiamo pensato, inoltre, che lavorare con protocolli standard come HTTP avrebbe semplificato notevolmente il progetto; Su CPAN. infatti vi sono molti moduli pronti all'uso per gestire questo protocollo e ciò avrebbe anche aiutato a rendere questa applicazione interoperabile.

Avevo due fonti principali per studiare i thread in Perl: il capitolo del "libro del cammello", "Programming Perl" di Wall, Christiansen ed Orwant ed il documento "perlthtut", che è incluso in tutte le distribuzioni di Perl recenti. Altre informazioni sono sparse nella documentazione dei moduli threads, threads::shared e Thread::Queue.

Dopo aver fatto un po' di esperimenti con le nuove cose che stavo imparando, ho deciso che era arrivato il momento di venire al sodo. Allora ho preso il libro "Learning Perl/tk", che avevo letto tempo fa, ma che non avevo mai messo in pratica, ed ho rivisto le parti che mi avrebbero permesso di creare una semplice interfaccia grafica: una finestra con un grosso riquadro dedicato alla visualizzazione del testo della conversazione, un campo dove inserire le risposte ed un bottone per inviare ciò che scriviamo.

Un semplice schema dell

Un semplice schema dell'applicazione: due thread che si scambiano informazioni usando una coda

Lo schema dell'applicazione era relativamente semplice, per prima cosa, è un'applicazione peer-to-peer: la lanciate indicando l'indirizzo e la porta tramite la quale volete contattare l'altra istanza del programma, a quel punto avrete a disposizione un'interfaccia in cui scrivere del testo e leggere le risposte (un po' come capita con il vecchio programma UN*X talk che probabilmente già conoscete). La comunicazione avviene utilizzando connessioni HTTP di tipo POST: per inviare un messaggio viene fatta una richiesta di tipo POST all'altro programma all'URI /message; la richiesta POST contiene due variabili: name, contenente il vostro alias, e message che contiene il messaggio da inviare. Per ricevere un messaggio, l'applicazione agisce come un server HTTP che gestisce richieste POST all'URI /message, formattato come descritto prima.

Il problema dei thread

Le moderne applicazioni effettuano molte operazioni allo stesso tempo. I browser per Internet, per esempio, devono fornire informazioni aggiornate sui processi interni mentre gestiscono molteplici connessioni. Gli elaboratori di testo devono assicurare risposte veloci ai comandi dell'utente e, se necessario, creare copie di sicurezza del lavoro in corso in caso di errori di sistema. I navigatori e gli elaboratori di testo (e riproduttori multimediali, giochi, programmi per la messaggistica istantanea...) sono esempi di applicazioni che utilizzano thread multipli.

Il termine multithreading si riferisce all'abilità di un'applicazione di dividere il lavoro in più thread che lavorano simultaneamente (od almeno così sembra). Mentre nel modello multiprocesso ognuno di essi lavora in un suo ambiente privato (e quindi sicuro), i thread, invece, condividono le stesse aree di memoria.

Le strutture dati e le classi sono dette 'a prova di thread' (thread-safe) quando sono in grado di serializzare l'accesso concorrente ai propri dati interni. Essi forniscono dei meccanismi per assicurare che, in un certo lasso di tempo, solamente un thread abbia accesso in lettura e scrittura alla memoria condivisa. Le strutture non adatte ai thread possono avere un comportamento non prevedibile ed è responsabilità del programmatore gestire l'accesso ai dati da parte di thread concorrenti. In base al linguaggio o alle librerie utilizzate, quindi, la programmazione con più thread può diventare più o meno complessa, facile da adottare o difficile da far funzionare. Allo stesso tempo le librerie possono essere parzialmente o totalmente adatte all'uso con i thread, spetta al programmatore scrivere le corrette strutture dati.

L'approccio tramite più thread semplifica la progettazione e l'implementazione delle applicazioni in tutti i casi nei quali possano essere definiti dei compiti paralleli e coordinati. Vi sono anche casi nei quali un approccio che non coinvolga più thread non è un'opzione accettabile. Nella Micro Edition di Java, per esempio, è obbligatorio gestire le connessioni alla rete usando thread separati per le connessioni.

I thread in Perl

Oltre alle normali precauzioni da prendere quando ci si occupa di thread, il Perl aggiunge nuove difficoltà. Nelle recenti versioni di Perl, per esempio, i dati non sono condivisi fra i thread, questo significa che dovete esplicitamente specificare i dati da condividere. Se decidete di fare ciò allora saprete che state andando in cerca di problemi e se cercate di rendere disponibili degli oggetti che contengono delle variabili globali all'interno delle vostre strutture dati (come, per esempio, oggetti che contengono riferimenti a file od a socket) state andando incontro a notevoli problemi. Questo obbliga chiunque stia creando un'applicazione Perl basata su thread utilizzando gli oggetti, ad essere molto cauto, oppure ad utilizzare una delle varie alternative, come POE, oppure a rinunciare ad i thread in Perl.

Oh, tra l'altro, un oggetto Thread::Queue può essere sì condiviso fra i thread, ma non potete condividere oggetti tramite esso.

Questi problemi, probabilmente, porteranno a credere che le interfacce Tk ed i thread non possano essere utilizzati bene insieme. In realtà è possibile, sempre che non cerchiate di condividere gli oggetti dell'interfaccia. L'applicazione che stiamo descrivendo non lo fa, in pratica c'è un thread principale, dove esistono l'interfaccia e gli oggetti, ed un secondo thread http che aspetta i messaggi in entrata. Quando è ricevuto un nuovo messaggio il thread http lo mette nella coda dell'interfaccia per visualizzarlo nella finestra principale del programma di chat. Utilizzando questo approccio anche quando un messaggio è inviato al ricevente, il testo è posto in coda e visualizzato, così c'è un sistema unico per l'aggiornamento. Gli oggetti dell'interfaccia del thread principale e l'oggetto httpd nell'altro thread non devono sapere nulla uno dell'altro: quando lo httpd deve inviare un messaggio all'esterno lo mette in coda; da parte sua, l'interfaccia controlla regolarmente se vi sono messaggi in attesa ed agisce di conseguenza. La GUI non deve sapere da dove arriva il messaggio e così, avendo una sola routine che legge dalla coda ed aggiorna l'interfaccia, si evita la complicazione di avere diversi thread che manipolano contemporaneamente il contenitore del testo.

L'applicazione in Perl

Per questa applicazione ho usato diversi moduli, alcuni di essi, come threads, threads::shared, Tk e Thread:Queue per nominarne alcuni, sono indispensabili; alcuni sono utili ma potrebbero essere considerati superflui, ma hanno almeno una caratteristica interessante che volevo utilizzare: HTTP::Daemon e HTTP::Status per il lato server e LWP::UserAgent per il lato client. Potete ridurre le risorse richieste dall'applicazione eliminando alcuni di questi moduli: siete liberi di fare quello che volete, è software libero!

Potete lanciare l'applicazione utilizzando la configurazione interna, in questo caso questa si collegherà alla porta 1080 di localhost e si metterà in ascolto su quella stessa porta, alla fine vi ritroverete a parlare con voi stessi. Se volete provare qualche cosa di più interessante, provate a collegarvi ad un'altra istanza della chat dando dei valori sensati ai parametri -peerhost, -peerport, -localport tramite linea di comando. Per esempio, se volete "giocare" con due finestre scrivete qualche cosa di simile:

bronto@marmotta:$ perl chath.pl -peerport 1081 -mynick bronto &
bronto@marmotta:$ perl chath.pl -localport 1081 -mynick maroon &

Questi comandi creeranno due finestre che comunicheranno una con l'altra. Notate che potete permettere all'applicazione di indovinare il vostro nome utente o potete suggerirne uno con l'opzione -mynick.

Dopo avere impostato l'infrastruttura richiesta (come la coda per lo scambio di messaggi e l'oggetto per inviarli) lanciamo il thread httpd.

my $httpdt = threads->new(&httpd) ;
$httpdt->detach ;
 

I dettagli del thread, che è una routine httpd, sono relativamente semplici: cerca di creare un oggetto HTTP::Daemon e sta in attesa di connessioni in ingresso; la chiamata ad accept() scade ogni dieci secondi, così può controllare se il thread principale richiede la chiusura del programma. Quando viene accettata una nuova connessione deve essere controllata (deve essere una richiesta POST all'URL /message), il messaggio è decodificato dal modulo CGI ed il testo è inserito nella coda in entrata.

Ritornando al thread principale, tutti i componenti dell'interfaccia sono creati, configurati ed infine inseriti nella finestra principale. Proprio prima del lancio del MainLoop dell'interfaccia e Tk, impostiamo l'area adibita alla rappresentazione del testo perché si aggiorni ogni 300 millisecondi (proprio così, 3 decimi di secondo):

$tbox->repeat(300,&update_chat_window) ;
MainLoop ;

Tutto, da questo punto in poi, funziona senza problemi. Ogni volta che un messaggio è inviato o ricevuto viene accodato; ogni 300 millisecondi l'interfaccia esegue update_chat_window e controlla se c'è qualche messaggio da estrarre dalla coda e da visualizzare.

Tutto ciò dura fino a quando non chiudete la finestra principale. Quando lo fate termina anche MainLoop ed il thread principale procede nell'esecuzione: aggiorna le variabili condivise $keep_running, segnalando al thread httpd di terminare appena possibile e di aspettare per un po'. Se, dopo dieci secondi, il thread httpd è ancora attivo, quello principale solleva un'eccezione e l'intera applicazione termina (beh, almeno spero!).

Thread in Java

Java offre un buon supporto per la programmazione multi-threading. Anche se non è ciò che c'è di meglio sul mercato permette agli sviluppatori di scrivere codice che coinvolga più thread con uno sforzo minimo. I thread sono "inclusi" nel cuore del linguaggio Java e nelle libreria di classi standard. "Tiger" (il nome in codice per l'ultima versione del Java Developement Kit, la 5.0) fornisce un supporto migliorato per la programmazione concorrente. In breve, scrivere un thread in Java vuol dire estendere la classe standard Thread oppure implementare l'interfaccia Runnable ed assegnarla ad un thread. Normalmente dovete usare il secondo approccio quando la vostra classe deve estendere una classe pre-esistente: poiché Java non supporta l'ereditarietà multipla, implementare l'interfaccia Runnable è obbligatorio. Comunque decidiate di fare, il cuore del thread è nel metodo run(). Java fornisce costrutti per sincronizzare l'accesso concorrente ai dati. Il costrutto wait-notify usato in questa applicazione di esempio permette al programmatore di sincronizzare i lavori dei thread: i thread sono in pausa (wait()) quando un messaggio è inviato e sono "risvegliati" (notify()) quando un nuovo messaggio è pronto per essere inviato.

L'applicazione Java

L'esempio di chat descritto in precedenza utilizza i thread per gestire i messaggi in ingresso. Nell'esempio in Java vedremo come usare i thread efficacemente per implementare l'invio dei messaggi.

Prima di iniziare diamo un'occhiata al lato server. La piattaforma Java fornisce un modello di applicazione Servlet che è una tecnologia allo stato dell'arte per i servizi aziendali. Questo è un approccio esagerato per le nostre necessità, abbiamo quindi scelto il server web Simple. È un programma sia potente (ed effettivamente semplice) sia un contenitore di Servlet che può essere inserito all'interno di altre applicazioni.

Per operare da server web l'architettura di Simple richiede che si estenda la classe BasicService. Le nostre sotto-classi devono solamente implementare il metodo process(), il quale riceve gli oggetti delle richieste e delle risposte usati per gestire la connessione corrente. Simple gestisce i suoi propri thread per le connessioni in entrata, cosicché il multithreading è nascosto all'interno della libreria. Noi dobbiamo avere a che fare solamente con i messaggi in uscita.

Per la parte client possiamo scegliere fra tre differenti implementazioni: bloccante, basata su thread multipli o singoli. Abbiamo scritto il metodo doSend() il quale riceve la stringa inviata dal programma remoto. doSend() ottiene una istanza HttpURLConnection, imposta il metodo della connessione (POST), codifica tutti i parametri e, alla fine, invia i dati attraverso la connessione e la chiude.

doSend()TT> è un sistema bloccante: non ritorna sino a quando i dati non sono stati trasferiti e la connessione è stata chiusa. Quindi, se non vogliamo che blocchi l'intera interfaccia, dobbiamo aggirare il problema ed utilizzare un sistema multithreading. Ecco perché usiamo doSyncSend, doMultiThreadSend e doSingleThreadSend().

Sistema sincrono: l'interfaccia è bloccata sino al completo invio del messaggio

Sistema sincrono: l'interfaccia è bloccata sino al completo invio del messaggio

Questo primo approccio è molto simile a quello adottato per la soluzione scritta in Perl: quando l'utente preme il bottone, doSyncSend() semplicemente invoca doSend(), quindi l'interfaccia è bloccata sino all'uscita di doSyncSend(). Per la nostra applicazione non è un problema critico, ma dobbiamo essere consci che l'interfaccia non sarà aggiornata sino a quando il messaggio non sarà effettivamente stato inviato.

Il secondo approccio, il metodo doMultiThreadSend() definisce e stanzia una classe interna anonima che estende il Thread standard. Il suo metodo run() invoca il metodo doSend(). Poiché doMultiThreadSend() è eseguito in thread separati, esce immediatamente e l'interfaccia, quindi, non rimane congelata. In breve: per evitare il blocco della GUI creiamo una procedura multithreading per l'invio dove un nuovo thread è creato tutte le volte che l'utente deve inviare un messaggio.

Un thread anonimo è creato ognivolta che è necessario inviare un messaggio. Il thread sarà distrutto dopo l\'invio. <tt>doMultiThreadSend()</tt> esce immediatamente e l\'interfaccia non si blocca.

Un thread anonimo è creato ogni volta che è necessario inviare un messaggio. Il thread sarà distrutto dopo l'invio. doMultiThreadSend() esce immediatamente e l'interfaccia non si blocca.

Creare e distruggere thread può essere molto oneroso in termini di risorse, è un approccio accettabile per il nostro piccolo programma, ma è generalmente sconsigliato per applicazioni più complesse. È per questo motivo che abbiamo affrontato un terzo approccio, il più popolare nelle applicazioni reali. Un thread singolo ed a sé stante (od un insieme di thread) è utilizzato per gestire i messaggi in uscita. Questo tipo di thread mantiene una coda dei messaggi e invia in serie.

Nel nostro esempio la classe Dispatcher riceve solamente un messaggio in uscita alla volta utilizzando uno schema wait-notify L'invio avviene all'interno del metodo run() il quale invia il testo e poi si ferma in attesa (wait()), quando un nuovo messaggio è fornito dal metodo send(), allora notifica l'oggetto che sblocca run().

Una singola istanza del thread Dispatcher gestisce i messaggi: manda qualsiasi messagio in uscita e rimane in attesa fino all'arrivo del successivo. <tt>doSingleThreadSend()</tt> ritorna immediatamente così come <tt>doMultiSend()</tt>

Una singola istanza del thread Dispatcher gestisce i messaggi: manda qualsiasi messaggio in uscita e rimane in attesa fino all'arrivo del successivo. doSingleThreadSend() ritorna immediatamente così come doMultiSend()

Conclusioni

Abbiamo appena intaccato la superficie: progettare ed implementare un'applicazione multithreading richiederebbe una conoscenza più profonda sia dei paradigmi della programmazione concorrente sia delle tecniche di programmazione del linguaggio da usare. Nel caso specifico del Perl, l'implementazione dei thread è lontana dall'essere soddisfacente e pronta per un uso serio, ma, come altre cose presenti in questo linguaggio, potete abituarvi ad essa e prepararvi per quando uscirà Perl 6.

Note e risorse

Gli autori

Marco Marongiu, nato nel 1971, si laurea in Matematica nel 1997; lavora full-time come system administrator per Tiscali nella sede di Sa Illetta a Cagliari. Per passione è programmatore Perl, articolista su riviste specializzate di informatica e relatore in seminari tecnici, e si interessa di tecnologie web e XML. È uno dei fondatori del Gruppo Utenti Linux Cagliari (GULCh), il primo nato in Sardegna. Da quando è diventato padre cerca disperatamente di riorganizzare la propria vita in modo da poter riprendere a scrivere articoli e tenere seminari.

Stefano Sanna attualmente si occupa di sviluppo di applicazioni su piattaforma Java ME presso Beeweeb Technologies, una brillante azienda specializzata in soluzioni per il mercato mobile. In precedenza è stato stato Expert Software Engineer nell'area Network Distributed Applications del CRS4, dove si è occupato di mobile computing, sistemi distribuiti e sistemi georeferenziati. Dal 2003 è articolista per le riviste DEV e Computer Programming, dove scrive su Java ME, applicazioni per dispositivi mobili, tecnologie wireless e Linux. Si occupa anche di sviluppo mobile, SOHO networking, tecnologie wireless e robotica LEGO, adeguamento al codice della privacy.

Il traduttore

Pietro Leone lavora come amministratore di sistema presso il Gruppo Abele di Torino e come consulente free-lance. Appassionato di informatica, utente Amiga, GNU/Linux e programmatore, molto dilettante, in ARexx, C, Perl e PHP. Altri hobby: storia militare, strategia, Giochi di Ruolo e lettura.

Sinistra <- Preseeding Debian Parte I - Indice Generale - Copertina - PLUTO Meeting 2007 -> Destra