MokaByte Numero 19 - Maggio 1998
 

Introduzione alla 
programmazione  concorrente
II parte
di
Massimo
Carli
I costrutti offerti da Java : vediamo gli strumenti offerti da Java per la programmazione concorrente

 



Nel precedente articolo abbiamo affrontato i concetti fondamentali di programmazione concorrente indipendentemente da un particolare linguaggio. Questo mese descriveremo gli strumenti che fanno di Java un ottimo linguaggio per il supporto della programmazione concorrente; Java sarà proprio lo strumento che utilizzeremo nella risoluzione del problema esposto il mese scorso: cucinare dei buoni piatti di spaghetti al sugo! Prima di proseguire, mi sembra doveroso sottolineare che per comprendere le tematiche affrontate in questo articolo, è necessario avere già una certa padronanza del linguaggio.

I processi

La prima cosa da fare è capire come si creano e si gestiscono i processi all'interno dei sorgenti Java. A dire il vero parleremo di "thread" anziché di processi: si tratta di un abuso linguistico, ma per gli scopi che ci proponiamo è un compromesso accettabile.
Il punto di partenza è dato dallo studio della classe java.lang.Thread, che dispone di alcuni metodi che permettono la gestione dei thread; i principali sono:
 

Per definire un thread è sufficiente estendere la classe java.lang.Thread e specificare le azioni che dovranno essere compiute, all'interno del metodo run().
 
public class MioThread extends Thread{
    public MioThread(){
        super();
    }
    public void run(){
                .....
    }
}// fine classe MioThread


Per utilizzare il thread, esso dovrà essere istanziato mediante una linea del tipo:

    MioThread thread= new MioThread();

A questo punto esso dovrà essere mandato in esecuzione mediante

    thread.start();

Così facendo verranno eseguite le azioni specificate all'interno del metodo run().
Una volta eseguita l'ultima azione, il thread termina e, se l'oggetto non è più referenziato, viene addirittura eliminato dal Garbage Collector.
Ricordate quindi che il metodo run() rappresenta il corpo del thread, ovvero l'insieme di istruzioni che verranno eseguite non appena verrà chiamato il metodo start(). Molto spesso il metodo run() contiene un ciclo infinito del tipo while(true){} per cui, per la sua terminazione, dobbiamo obbligatoriamente usare stop() (terminazione forzata). Dopo l'esecuzione di stop() il thread è soppresso e non potremo eseguire nuovamente start().
Abbiamo visto che per la sospensione del thread esistono due diversi metodi. Il metodo yield() si può applicare ad un thread in esecuzione, il quale dà la precedenza ad altri thread di uguale priorità (il funzionamento reale di questo metodo dipende moltissimo dalla implementazione della Java Virtual Machine e dal sistema operativo). Il metodo suspend(), invece, blocca il thread che non tornerà in esecuzione fino a quando non verrà invocato il metodo resume().
Il metodo join() permette ad un thread di bloccarsi in attesa che un altro termini la sua esecuzione.
Prima di passare ad utilizzare questi metodi, vediamo un'ultima cosa.
Nel caso in cui la nostra classe sia già l'estensione di un'altra e non essendoci ereditarietà multipla in Java, si utilizza l'interfaccia java.lang.Runnable che prevede la definizione del solo metodo run().
Il procedimento da seguire è il seguente:

    public void run(){
    . . .
    . . .
    }

    public void start(){
        if (runner==null){
            runner= new Thread(this);
            runner.start();
        }
    }// fine start()

    public void stop(){
        if (runner!=null){
            runner.stop();
            runner=null;
        }
    }// fine stop()

  }// fine classe

Notiamo come, in questo caso, debba essere definita una variabile di tipo Thread che si dovrà poi gestire all'interno dei metodi start() e stop() (che, si badi bene, non sono ereditati dalla classe Thread).
 
 
 

Dalla teoria alla pratica
Proviamo, quindi, ad applicare quanto visto al problema della realizzazione di un piatto di spaghetti al sugo. Il mese scorso abbiamo scomposto l'algoritmo in tre parti principali:
 

    Preparare il sugo
    Cuocere gli spaghetti
    Mettere il tutto nel piatto
Possiamo rappresentare la preparazione della ricetta attraverso una classe astratta che chiamiamo SpaghettiAlSugo che lascia indefinito il corpo del metodo prepara_spaghetti()
 
 
 

Listato1: classe astratta Filosofo
 

/**
* Classe astratta che rappresenta il Thread relativo  a  ciascun
* filosofo. Per scegliere la politica di gestione delle risorse
* basterà estendere questa classe e ridefinire il metodo
* gestione_risorse().
*/

public abstract class Filosofo extends Thread{
        protected  String nome;
        public Filosofo(){
                this("");
        }// fine costruttore vuoto
       public Filosofo(String nome){
                super();
                this.nome=nome;
        }// fine costruttore
       public void run(){
                while (true){
                gestione_risorse();
        }// fine while
   }// fine run

    public abstract void gestione_risorse();
    }// fine classe filosofo


In questo modo, per specificare un certo procedimento ci basterà estendere la classe SpaghettiAlSugo e definire il metodo prepara_spaghetti().
La classe utilizza un certo numero di risorse che abbiamo memorizzato in un array (risorse[])
 
 
 

Listato2: filosofo semaforo
/**
  * Utilizzo dei semafori binari per l'allocazione
  * delle risorse
  */

public class FilosofoSemaforo extends Filosofo {
         protected Semaforo [] bastoncini;
         protected int who;
 

          public FilosofoSemaforo(int who,Semaforo[] bastoncini){
                  super(""+who);
                  this.bastoncini=bastoncini;
                  this.who=who;
          }// fine costruttorevuoto

          public void gestione_risorse(){
            System.out.println("Il filosofo "+nome+" Pensa");
            long attesa= (long)(Math.random()*10000);
            try{Thread.sleep(attesa);}catch(InterruptedException ie){}
            System.out.println("Il filosofo "+nome+" Vuole il bastoncino destro");
            bastoncini[(who+1)%5].P();
            System.out.println("Il filosofo "+nome+" Ottiene il bastoncino destro");
            System.out.println("Il filosofo "+nome+" Vuole il bastoncino sinistro");
            bastoncini[who].P();
            System.out.println("Il filosofo "+nome+" Ottiene il bastoncino sinistro");
            System.out.println("Il filosofo "+nome+" Mangia");
            attesa= (long)(Math.random()*10000);
            try{Thread.sleep(attesa);}catch(InterruptedException ie){}
            System.out.println("Il filosofo "+nome+" Rilascia il bastoncino destro");
            bastoncini[(who+1)%5].V();
            System.out.println("Il filosofo "+nome+" Rilascia il bastoncino sinistro");
            bastoncini[who].V();
          }// fine gestione risorse

 }// fine classe
 
 
 

La classe Risorsa prevede solamente l'assegnamento di un nome .
Possiamo infatti associare a ciascuna delle tre azioni una risorsa fondamentale per portarla a compimento (in realtà le risorse sarebbero in numero maggiore ma il nostro interesse è la descrizione dei concetti fondamentali e non la risoluzione completa del problema), ovvero:

Le risorse che dovremo creare saranno dunque "tegamino", "pentola" e "piatto".
Se osserviamo la classe SpaghettiAlSugo notiamo anche che si vuole cucinare un piatto di spaghetti dietro l'altro, per cui abbiamo utilizzato un ciclo while infinito. Rappresenteremo, poi, ciascuna azione con un'istanza della classe Azione, il cui metodo esegui()non fa altro che fornire una descrizione ed aspettare un tempo casuale per il completamento
 
 
 

Listato 3

 
/**
    * Verifica dell'utilizzo dei semafori per la gestione
    * delle risorse
    */

           public class ProvaFilosofoSemaforo {
                   public static void main(String str[]){
                           // Creiamo il vettore di semafori
                           Semaforo[] bastoncini=new Semaforo[5];
                           for (int i=0;i<5;i++){
                                   bastoncini[i]= new Semaforo();
                           }// fine for
                           // Creiamo vettore filosofi e li avviamo
                           FilosofoSemaforo[]      filosofi= new FilosofoSemaforo[5];
                           for (int i=0;i<5;i++){
                                   filosofi[i]= new FilosofoSemaforo(i,bastoncini);
                                   filosofi[i].start();
                           }
                }// fine main
          }// fine classe


 

A quest'ultimo passiamo in input anche la risorsa utilizzata per eseguire l'azione; la risorsa sarà presa e, al termine, rilasciata.
Proviamo adesso a dare una prima definizione del metodo prepara_spaghetti()
 
 
 

Listato 4:
/**
  * Creazione di un Monitor per l'allocazione globale
  * delle risorse
  */

  public class GestoreBastoncini {
          protected byte num_bast[]={2,2,2,2,2};
          public synchronized void acquisizione_bastoncini(int who){
                  while (num_bast[who]<2){
                          try{
                                  wait();
                          }catch(InterruptedException ie){}
                  }// fine while
                  num_bast[(who+1)%5]--;      // Prendiamo il bastoncino a destra
                  num_bast[(who+4)%5]--;     // Prendiamo il bastoncino a sinistra
          }// fine acquisizione_bastoncini

          public synchronized void rilascio_bastoncini(int who){
                  num_bast[(who+1)%5]--;    // Rilasciamo il bastoncino a destra
                  num_bast[(who+4)%5]--     // Rilasciamo il bastoncino a sinistra
                  notifyAll();
          }// fine acquisizione_bastoncini
 }// fine classe

public void prepara_spaghetti(){
    Azione.esegui(cuoco," Preparara il sugo",  risorse[0]);
    Azione.esegui(cuoco," Cuoce gli spaghetti",  risorse[1]);
    Azione.esegui(cuoco," Mette il tutto nel  piatto", risorse[2]);
}
 

Se eseguiamo l'applicazionePreparaSpaghetti1, notiamo che le azioni si susseguono in maniera corretta ed il nostro problema sembrerebbe risolto in quanto viene cucinato un piatto di spaghetti al pomodoro alla volta.
Come accennato il mese scorso, i problemi sorgono quando si vogliono cucinare più piatti di spaghetti contemporaneamente. Proviamo la soluzione più immediata, ovvero creiamo l'applicazione PreparaSpaghetti2 modificando il metodo main()in modo da creare due istanze da avviare una dopo l'altra. In questo caso utilizziamo un costruttore che ci permetta di dare un nome a ciascun thread-"cuoco".
 
 
 

L'output diventa il seguente:


I lettori più attenti noteranno che qualcosa non funziona in quanto, disponendo di un solo tegamino, non è possibile che entrambi i cuochi se ne impossessino. Se un primo cuoco si impossessasse della risorsa tegamino, il secondo cuoco dovrebbe attendere che si renda disponibile ovvero che il primo lo rilasci. Sono quindi necessarie delle tecniche opportune di sincronizzazione.
 
 

Tecniche di sincronizzazione: i semafori binari
Nel paragrafo precedente abbiamo visto come un programma, all'apparenza corretto, possa presentare dei grossi problemi se guardato in un contesto multithreading. In un contesto, cioè, in cui più processi dello stesso programma devono condividere le stesse risorse. La condivisione di una risorsa è un problema tipico di programmazione concorrente: un processo vuole impossessarsi di una particolare risorsa e si blocca in attesa che questa si renda disponibile. La soluzione classica a questo problema si chiama semaforo binario.
Supponiamo di associare il semaforo ad una particolare risorsa condivisa (il tegamino). Se un processo vuole acquisire il tegamino eseguirà una particolare funzione del semaforo che, per motivi storici, si indica con P() (oppure wait()). Se la risorsa è disponibile il processo la utilizzerà ed il semaforo ne memorizzerà lo stato (semaforo rosso). Se un altro processo, a questo punto, prova ad impossessarsi della stessa risorsa, eseguirà nuovamente la P() sullo stesso semaforo, ma questa volta rimarrà bloccato. Questo succederà quindi per tutti gli eventuali processi che facciano la stessa richiesta.
Quando il processo utilizzatore rilascia la risorsa, esegue un'altra operazione sul semaforo che si indica con V() (oppure signal()).A questo punto, se sono presenti dei processi in coda, uno di essi acquisisce la risorsa facendo rimanere rosso il semaforo. Se non sono presenti processi in coda il semaforo diventerà verde ovvero, la risorsa sarà disponibile.
In Java possiamo affermare che un semaforo binario è un oggetto che dispone di due metodi, P() e V() appunto, che regolano l'acquisizione di una particolare risorsa. La caratteristica fondamentale di questi metodi è l'atomicità. Dire che un metodo o una funzione è atomica, significa affermare che la sua esecuzione non può essere interrotta: è come una operazione primitiva del sistema operativo.
 

Creazione del semaforo in Java
Vediamo allora di realizzare il semaforo in Java introducendo altri importanti strumenti del linguaggio.
Dire che un processo utilizza una risorsa condivisa significa affermare che il processo ha accesso esclusivo alla risorsa, ed agisce quindi da solo su di essa. L'insieme di operazioni che vengono fatte su una risorsa a cui si ha accesso esclusivo, si chiama regione critica. Per delimitare una regione critica, Java mette a disposizione la parola chiave synchronized. Se, ad esempio, vogliamo disporre in modo esclusivo di oggetto basterà mettere le operazioni volute, all'interno di un blocco del tipo:
 

synchronized (oggetto){
    // Regione Critica
    . . .
}
In questo modo solo un processo alla volta accederà alle operazioni della regione critica. La parola chiave synchronized può essere utilizzata anche come modificatore nel caso in cui la regione critica corrisponda al corpo di un intero metodo, cioè:

   public synchronized void MioMetodo(){
        // Regione critica
        ...
    }// fine metodo

che è equivalente a

   public void MioMetodo(){
        synchronized(this){
            // Regione Critica
            ...

        }
    }

In questo caso l'esclusività è legata all'oggetto istanza della classe a cui il metodo appartiene: due o più processi non possono eseguire lo stesso metodo contemporaneamente sullo stesso oggetto (this). Ovviamente l'esclusività non vale nel caso di istanze diverse. A questo punto il lettore potrebbe dire di aver già gli strumenti per realizzare un semaforo, mettendo cioè le operazioni che lavorano sulla risorsa all'interno di un blocco synchronized associato alla risorsa stessa. Il risultato può sembrare lo stesso, e non si utilizzerebbero i due metodi, P() e V(), che caratterizzano il semaforo binario. Inoltre, esiste un classico problema di programmazione concorrente che il solo synchronized non può risolvere.
Supponiamo, infatti, che un processo acquisisca una risorsa in modo esclusivo. Supponiamo inoltre che, ad un certo punto, lo stesso processo, per proseguire, desideri che si verifichi una particolare condizione (ad esempio che siano disponibili dei dati su uno stream). Ma affinché tale condizione possa verificarsi, potrebbe essere necessario il rilascio della risorsa posseduta in modo esclusivo. In tal caso, si andrebbe incontro ad un cosiddetto "deadlock" (o "stallo"), ovvero ad una condizione "irrisolvibile". Per questo, il solo costrutto synchronized non è sufficiente.
Java mette a disposizione di ogni oggetto i seguenti metodi:

Essi possono essere utilizzati solamente all'interno di un blocco synchronized associato alla stessa risorsa. Come conseguenza non possiamo dire che le operazioni di un semaforo P() e V() sono equivalenti ai metodi wait() e notify(). Infatti il semaforo deve regolare l'ingresso ad una regione critica e non essere già parte di essa. Possiamo pensare, però, di realizzare un nostro oggetto Semaforo (vedi listato 5) sfruttando tutti gli strumenti Java finora introdotti. Il fatto che i metodi P() e V() siano atomici si può esprimere attraverso il modificatore synchronized.
 

Listato 5
 

Il fatto della disponibilità o meno della risorsa, si esprimerà con una variabile intera stato, che può valere 1 (semaforo verde) oppure 0 (semaforo rosso).
Utilizziamo anche un contatore (in_attesa) che memorizza il numero dei processi in attesa, ovvero che hanno eseguito la P() sul semaforo che era nello stato di rosso. Osservando il listato, vediamo che entrambi i metodi sono definiti synchronized in modo che solo uno di essi possa essere eseguito in un particolare istante.
Successivamente il procedimento è abbastanza semplice; se lo stato del semaforo è ad 1 (verde), possiamo impossessarci della risorsa e mettere lo stato del semaforo a 0 (rosso). Se lo stato del semaforo è rosso, ci mettiamo in attesa richiamando la
wait(). La V() consiste, invece, nel verificare se sono presenti processi in attesa da abilitare o meno attraverso il metodo notify(). Nel caso non vi siano processi in attesa, lo stato del semaforo verrà messo nuovamente a 1 (verde).
L'aggettivo binario deriva dal fatto che il semaforo può assumere solamente due stati, 0 e 1, in quanto la risorsa è unica. Nel caso in cui le risorse disponibili siano n il semaforo avrà un funzionamento analogo bloccandosi solamente nel caso in cui tutte le n risorse non fossero più disponibili. Per implementare un semaforo di quel tipo basterà mettere porre stato=n.
Ed ecco l'utilità dei due costruttori utilizzati nella classe Semaforo.
 
 

Iniziamo a cucinare...
Ora che abbiamo creato degli strumenti che permettono di regolare l'assegnazione delle risorse, e quindi di stabilire una certa politica di scheduling, vogliamo applicarli al nostro problema. Abbiamo infatti visto che la semplice esecuzione dei due thread era esatta da un punto di vista formale (l'applicazione "girava") ma non certo dal punto di vista dei risultati (entrambi utilizzavano contemporaneamente le stesse risorse).

Creiamo allora l'applicazione PreparaSpaghettiMutex.
Il metodo prepara_spaghetti() è diventato:

    public void prepara_spaghetti(){
        semafori[0].P();
        Azione.esegui(cuoco," Preparara il sugo", risorse[0]);
        semafori[0].V();
        semafori[1].P();
        Azione.esegui(cuoco," Cuoce gli spaghetti",
        risorse[1]);
        semafori[1].V();
        semafori[2].P();
        Azione.esegui(cuoco," Mette il tutto nel  piatto", risorse[2]);
        semafori[2].V();
    }

Prima dell'acquisizione della risorsa si chiama la P() del semaforo ad esso associato, e per il rilascio si chiama il metodo V(). Se il lettore esegue l'applicazione può verificare che, questa volta, tutto procede nel modo corretto. Il lettore potrebbe anche verificare il funzionamento della nostra applicazione nel caso di un numero di risorse superiore utilizzando il costruttore appropriato della classe Semaforo.
 
 

Conclusioni
Questo mese abbiamo introdotto la maggior parte degli strumenti che fanno di Java un ottimo linguaggio multithreading. Attraverso l'introduzione del costrutto synchonized e dei metodi wait() e notify(), abbiamo costruito uno strumento alla base di molte tecniche di programmazione concorrente: il semaforo. Abbiamo poi visto come applicare il semaforo binario alla risoluzione di uno dei nostri problemi iniziali, ovvero la condivisione delle risorse.
Realizzare correttamente delle applicazioni multithreading non è cosa semplice; cosa succederebbe se i cuochi fossero più di due? Se dal punto di vista delle risorse tutto funziona correttamente (ogni risorsa è utilizzata da un solo processo in un determinato istante), cosa possiamo dire dal punto di vista dei processi?
Nel nostro caso, siamo sicuri che ogni cuoco riesca ad impossessarsi delle risorse?
Il prossimo mese cercheremo di rispondere a queste domande.
 
 

Bibliografia
[1] "Principi e tecniche di programmazione concorrente", Ed. UTET, Paolo Ancillotti, Maurelio Boari.
[2] "Introduzione al multiprocessing ed al multithreading", Dev 36 Dicembre 96, Salvatore Antonio Bello.
[3] "Concurrent Programming in Java - Design and Pattern", Doug Lea, Addison Wesley.
[4] "Java in a NutShell" 2nd Edition, David Flanagan, O'Reilly.
[5] "Java Restaurant",F.Tisato, L.Nigro, Apogeo.
[6] "Java Threads", Scott Oaks, Henry Wong, O'Reilly.

  

 

MokaByte rivista web su Java

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