MokaByte
Numero 19 - Maggio 1998
|
|||
|
programmazione concorrente II parte |
||
Massimo Carli |
|
||
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:
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:
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).public class MyThreadFrame extends Frame implements Runnable{
public void run(){
private Thread runner;
public MyThreadFrame(){
super();
. . .
}
. . .
. . .
}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
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:
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 runpublic 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:
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: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.
/**
* 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_bastoncinipublic 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 classepublic 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]);
}
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){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è:
// Regione Critica
. . .
}
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:
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 ricerca
nuovi collaboratori
|
||
|