MokaByte Numero 22 - Settembre 1998
Programmazione Concorrente 
di 
 Massimo Carli   
I thread in pratica:questo mese applichiamo a problemi reali i concetti visti nelle puntate precedenti (parte V)  

 


Nei mesi scorsi abbiamo introdotto i concetti fondamentali di programmazione concorrente e descritto i costrutti classici di semaforo e monitor. Abbiamo sfruttato la semplicità ed il supporto al multithreading di Java per studiare i problemi di blocco critico ed individuale cercando di dare soluzioni di carattere generale. Dopo molti concetti teorici, questo mese vediamo quali sono i processi sempre presenti in un applet o in un'applicazione Java, concludendo con un esempio pratico in cui risalti tutta la potenza e semplicità di programmazione dei Thread Java.

I Thread in una applicazione Java
Sappiamo che quando viene eseguita una applicazione Java, esiste un thread di default che esegue il metodo main(). In realtà i thread sono più di uno e si possono raggruppare in:

System Thread Group (Gruppo dei thread di sistema)
Main Group (Gruppo del thread principale)
Il System Thread Group dispone di quattro thread demoni che forniscono alcune delle funzionalità descritte nelle specifiche JVM (Java Virtual Machine). Questi thread sono: Il Main Group contiene, inizialmente, il solo: Non appena una applicazione Java crea una finestra per la gestione di una certa interfaccia grafica, vengono creati altri thread che vengono aggiunti al gruppo del thread principale e permettono di gestire gli eventi legati alla gestione del sistema delle finestre. Essi sono:
  I gruppi di thread sono legati tra loro da una relazione gerarchica ad albero. La radice di questo albero è proprio il System Thread Group mentre il Main Group è suo figlio.
 
Che cosa sono i gruppi di thread

Ma cos’è un gruppo di thread? Senza entrare nel dettaglio, supponiamo di avere un applet in cui esistono diversi 
processi in esecuzione. Possiamo avere, per esempio, un processo che carica delle immagini, uno che legge da una connessione socket verso il server, ecc. Nel caso in cui l’utente cambiasse pagina, il browser chiama il metodo stop() dell’applet. Bisognerebbe, quindi, ridefinire lo stesso metodo in modo da chiamare il metodo stop() di ciascun processo. 
L’oggetto java.util.ThreadGroup ci viene in aiuto in quanto ci permette di unire i vari thread in un unico gruppo. Per fermare tutti i thread del gruppo, basterà chiamare il metodo stop() dell’oggetto ThreadGroup. I gruppi di thread non sono solo insiemi di thread ma permettono di organizzare i thread secondo una struttura ad albero. Ogni gruppo di thread, tranne quello principale (root) dispone di un gruppo genitore. Questa organizzazione è alla base del meccanismo di sicurezza negli applet gestito, come sappiamo, dal SecurityManager. Tutti i thread di un applet appartengono ad uno stesso gruppo e nel SecurityManager esistono metodi che permettono di impedire a thread di un gruppo di accedere a quelli di un altro.

 

I Thread in un applet
I thread che abbiamo descritto per le applicazioni esistono anche negli applet. Quando il browser scarica un applet, crea un altro gruppo di thread che diviene figlio del Main Thread. In seguito, dentro il gruppo di thread creato, il browser crea un nuovo thread responsabile dell'esecuzione dell'applet.

Prima di passare alla descrizione dell'utilizzo dei thread in Java per i problemi più comuni, diamo una dimostrazione della esistenza dei thread appena descritti. Supponiamo di creare un applet che permette di scrivere una particolare stringa dopo un conteggio alla rovescia.

 

/************************************************* 
*       Applet Esempio1 
*       Questo applet dimostra la presenza del thread 
*       dell'applet creato dal browser, del Clock Handler 
*       e dello ScreenUpdater. 

*@param author: Massimo Carli 
*@paran date    : 04/04/1998 
*************************************************/ 
 
 

import  java.applet.*; 
import  java.awt.*; 

public class Esempio1 extends Applet { 
      protected int num; 
      public void init(){ 
         num=10; 
      }// fine init 
      public void paint(Graphics g){ 
         if (num==0){ 
           // Se num=0 abbiamo finito il conteggio per cui 
           // visualizziamo il messaggio 
           g.drawString("PARTITO!! ",10,20); 

           // Rimettiamo il conteggio a 10 
           num=10; 
         } 
         else { 
           // Altrimenti scriviamo il valore raggiunto 
           g.drawString((num-)+"",10,20); 
         } 
         // Facciamo la prossima repaint dopo 1 secondo 
         // ovvero 1000 millisecondi 
         repaint(1000); 
      }// fine paint 

      public boolean mouseDown(Event e, int x, int y){ 
      System.out.println("Premuto "+e); 
      return false; 
   } 
  }// fine Esempio1

Listato 1: Esistenza del Clock Handler e dello ScreenUpdater 

 

L'applet è molto semplice e consiste nella esecuzione della repaint() ripetuta ogni secondo.
Per eseguire una repaint() dopo un certo numero (delay) di millisecondi, si utilizza il metodo paint(int delay) della classe java.awt.Component. Se esistesse un unico thread nel periodo di attesa tra una repaint() e la successiva, l'applet sarebbe bloccato per cui certi eventi del mouse, per esempio, non sarebbero sentiti che dopo un secondo.

Il fatto che si possa attendere un certo intervallo dimostra la presenza del Clock Handler; il metodo repaint() chiede al Clock Handler di svegliarlo dopo un secondo; a quel punto, lo stesso Clock Handler, si dovrà preoccupare di far chiamare il metodo update(). Questo significa che il Clock Handler deve coordinarsi anche con lo Screen Updater che chiamerà il metodo update() quando il Clock Handler glielo dirà. Intanto il thread creato dal browser per l'applet continuerà la sua esecuzione facendo ritornare il metodo paint(). Il fatto che gli eventi del mouse vengano gestiti dimostra, inoltre, l'esistenza del thread AWT-Toolkit.

Un esempio pratico
Per dimostrare quanto utili possano essere i Thread nella programmazione Java, facciamo un esempio pratico.
Creiamo un applet che permette di visualizzare un insieme di messaggi in modo scorrevole. I messaggi sono letti da server e risiedono in un file testo. L'applet, non appena caricato, dovrà leggere il file testo contenente i messaggi e, se il tutto ha funzionato correttamente, dovrà iniziare a visualizzarli uno dopo l'altro. Durante la lettura del file di testo, l'applet dovrà visualizzare un messaggio di loading ed eventualmente avvisare se si è presentato qualche problema come la mancanza del file di testo. Come ultima specifica, facciamo in modo che se il contenuto del file testo non è stato caricato entro 1 secondo, venga visualizzato un messaggio di errore. Il problema si articola quindi, nelle seguenti fasi:

1) Caricamento dell'applet e visualizzazione del messaggio di loading

2) Lettura del file con partenza del timer per il time-out di caricamento

3) Comunicazione dell'avvenuto caricamento con passaggio del contenuto del file testo

4) Avvio della visualizzazione.

Da una veloce analisi del problema risalta la necessità di utilizzare processi diversi per assolvere funzioni parallele diverse. Realizziamo quindi una classe Timer (Listato 2) che permette di attendere un determinato periodo di tempo, scaduto il quale verrà chiamato un particolare metodo degli oggetti interessati.

 

/*************************************************** 
* Classe Timer 
* Questa classe rappresenta un Timer che, se il flag repeat 
* è true, richiama il metodo tic() di ogni suo ascoltatore 
* ogni delay millisecondi. Se il flag repeat è false, il metodo 
* tic sarà chiamato solo una volta. 
*@param author: Massimo Carli 
*@paran date    : 04/04/1998 
***************************************************/ 
 
 
 
 

import  java.util.Vector; 
public class Timer extends Thread  { 

// Numero di millisecondi da attendere 
private  int delay; 

// Indica se il metodo tic() deve essere richiamato 
// periodicamente (true) o no (false) 
private boolean repeat; 

// Insieme degli ascoltatori del Timer 

private Vector  ascoltatori; 

/** 
* Costruttore vuoto. 
*/ 
public Timer(){ 
    this(1000,false); 
}// fine costruttore 

/** 
*Costruttore con ritardo 
*@param delay : ritardo 
*/ 
public Timer(int delay){ 
    this(delay,false); 
}// fine costruttore 
 

/** 
*Costruttore completo 
*@param delay   : ritardo 
*@param repeat  : se true il metodo tic() viene chiamato ripetutamente 
*/ 
public Timer(int delay,boolean repeat){ 
   super("Timer");   // Creiamo un Thread di nome Timer 
   this.delay=delay; // Riferimento al ritardo in millisecondi 
   this.repeat=repeat;   // Riferimento 
   ascoltatori = new Vector(); // Creiamo l'insieme degli ascoltatori 
}// fine costruttore 
 

// Corpo del thread 
public void run(){ 
   while(true){ 
      try{ 
         Thread.sleep(delay); 
      }catch(InterruptedException e){} 
      notifica(); 
      if (!repeat) return; 
   }// fine while 
}// fine run 
 

// Questo metodo richiama il metodo tic() di 
// ciascun ascoltatore 
private final void notifica(){ 
  Vector copia= new Vector(); 
  synchronized(ascoltatori){ 
    copia=(Vector)(ascoltatori.clone()); 
  } 
for (int i=0;i 
...... 
 

Listato 2: Class Timer

 

Per ottenere questo effetto ci serviamo dell'interfaccia TimerListener (Listato 3).

 

/*************************************************** 
*       Interface TimerListener  *                                                                                                                                                                      * 
*       Questa interfaccia sarà  implementata   da ogni oggetto 
*       ascoltatore del Timer. Il Timer richiamerà il metodo tic() 
*       di   ogni oggetto ascoltatore. 
*  *                                                                                                                                                                       *@param author: Massimo Carli 
*@paran date    :04/04/1998 
***************************************************/ 
 
 

public interface TimerListener  { 
          /** 
          * Il Timer richiamerà il metodo tic di ogni oggetto 
          * ascoltatore che implementa questa interfaccia 
          */ 

          public void tic();  

  }// fine interface

Listato 3: Interfaccia TimeListener

 

Essa prevede semplicemente la descrizione del metodo tic() che verrà chiamato non appena il tempo di time_out, settato nel costruttore di Timer, sarà scaduto. La classe Timer, alla luce di quanto visto nei mesi scorsi, è molto semplice. Essa estende la classe Thread e definisce il metodo run() in modo tale da attendere il numero di millisecondi specificati nella variabile delay. Per la sospensione del Thread utilizziamo il metodo sleep(), che abbiamo scoperto essere regolato dal Clock Handler. Se un oggetto è interessato allo scadere del time_out dovrà semplicemente implementare l'interfaccia TimerListener e registrarsi ad esso chiamando il suo metodo addTimerListener().
Un comportamento analogo è svolto dalla classi Loader (Listato 4)
 

/*************************************************** 
 *       Classe  Loader 
 *       Questa classe permette di caricare un le righe di un 
 *       file testo dato il suo URL.     Il contenuto delle righe sarà 
 *       contenuto in un array di stringhe. 
 *@param author: Massimo Carli 
 *@paran date    : 04/04/1998 
 ***************************************************/ 

  import   java.util.Vector; 
  import   java.io.*; 
  import   java.net.*; 

 public class Loader extends Thread  { 

        // URL da cui scaricare il contenuto del file 
        private  URL   url; 

        // Insieme degli ascoltatori del Timer 
        private  Vector          ascoltatori; 

          /** 
          *Costruttore con ritardo 
          *@param delay : ritardo 
          */ 

          public Loader(URL url){ 
                  super("Loader");        // Creiamo il thread di nome "Loader" 
                  this.url=url; 
                  ascoltatori= new Vector(); 
          }// fine costruttore 

          // Corpo del thread 
          public void run(){ 
                  try{ 
                          InputStream in= url.openStream(); 
                          DataInputStream din= new DataInputStream(in); 
                          Vector dati= new Vector(); 
                          while(din.available()>0){ 
                                  dati.addElement(din.readLine()); 
                          } 
                          String[] ret=new String[dati.size()]; 
                          .........

Listato 4: Class Loader
e dall'interfaccia LoaderListener (Listato 5).
 
/*************************************************** 
*       Interface LoaderListener 
*       Questa interfaccia sarà  implementata da ogni oggetto 
*       ascoltatore del Loader.  Non  appena il loader ha caricato 
*       le informazioni volute, chiamerà il metodo loaded() degli 
*       ascoltatori passando un codice esplicativo dell'esito, 
*       ed uno caratteristico della struttura caricata. 

*@param author: Massimo Carli 
*@paran date    : 04/04/1998  

***************************************************/ 
 

public interface LoaderListener  { 
        /** 
        * Caricamento avvenuto in modo corretto 
        */ 
        public  int             OK=0; 
 

        /** 
        * Caricamento avvenuto in modo errato 
        */ 
        public  int             ERROR=1; 

       /** 
       * Il Loader richiamerà il metodo loaded di ogni oggetto 
       * ascoltatore che implementa questa interfaccia 
       */ 
       public void loaded(int esito,Object obj); 
}// fine interface 
 

Listato 5: Interfaccia LoaderListener 
 

La differenza consiste semplicemente nel corpo del thread che, in questo caso, legge da uno stream ottenuto dall'url che identifica il file testo contenente le stringhe da visualizzare. Quando il file testo è stato completamente letto, viene chiamato il metodo loaded() di ciascun ascoltatore di Loader e verranno passate le informazioni caricate insieme all'esito del caricamento stesso. Veniamo, allora, alla classe Scroller (Listato 6) che rappresenta l'applet per lo scrolling. Esso deve essere ascoltatore sia di Timer che di Loader per cui implementerà le interfacce TimerListener e LoaderListener rispettivamente. Il metodo alla base del funzionamento dell'applet è il metodo start() e si preoccuperà di creare ed avviare il timer ed il loader. A questo punto si tratterà di attendere quale tra i metodi tic() e loaded() verrà chiamato per primo. Nel caso del metodo tic() verrà visualizzato un errore in quanto il tempo di time_out è scaduto prima dell'avvenuto caricamento dei dati. Nel caso del metodo loaded() significa che l'insieme dei dati è a nostra disposizione per cui si potrà finalmente avviare il thread dell'applet. Ma per quale motivo sono necessari tutti questi thread?

Per quello che riguarda il thread della classe Timer possiamo dire di aver creato un thread simile al Clock Handler con l'importante differenza di poterlo facilmente controllare con i metodi classici della classe Thread (start(), stop(), ecc.). Per quello che riguarda la classe Loader la necessità dell'utilizzo di un Thread è dovuta alla natura bloccante della lettura da stream in Java. Possiamo pensare ad uno stream in Java come ad un canale da cui estrarre (nel caso di stream di lettura) un certo insieme di dati. Se si prova ad estrarre un dato da uno stream che è momentaneamente vuoto, il processo responsabile della lettura si blocca fino all'arrivo di nuovi dati.
È chiaro che se il processo che legge dallo stream è l'unico processo presente, si ha il blocco dell'intera applicazione o applet. Attraverso l'utilizzo dei thread si fa in modo che il processo in lettura attenda la disponibilità di dati che un eventuale altro thread gli potrà fornire.
 

Conclusioni
Java offre un supporto multithreading molto interessante anche se è, questo, un argomento intrinsecamente spinoso e difficile da capire. Durante questo mini corso sono stati affrontati gli argomenti in modo da dare una buona conoscenza di base, che potrete espandere con i numerosi ed interessanti testi sull'argomento.

Bibliografia


 
 
 
 

MokaByte Web  1998 - www.mokabyte.it 
MokaByte ricerca nuovi collaboratori. Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it