edutecnica

Concorrenza

        

La possibilità di mantenere in esecuzione più programmi contemporaneamente viene indicata multiprogrammazione (o multitask) e non è una funzionalità che è sempre esistita. Ad esempio con MS-DOS non era possibile fare funzionare due programmi simultaneamente. Con l'avvento dei processori a 32 bit questa prerogativa è stata resa disponibile; ragione per cui non c'è niente che ci impedisca di aprire a di far funzionare più programmi contemporaneamente.

Con questa filosofia, i programmi competono per l'uso delle risorse ed in particolare per la CPU. I vari programmi, vengono fatti funzionare in 'time-sharing' (condivisione di tempo) cioè la CPU suddivide il suo compito su di essi per piccole quantitò di tempo e lo fa così rapidamente che per la nostra percezione, questo processo ha delle caratteristiche di continuità o meglio di simultaneità. Anche la Java Virtual Machine consente di eseguire più attività contemporaneamente; questa possibilità è nota come multithread, di comunicazione che unisce una sorgente ad una destinazione.

Il thread è un processo che viene eseguito in un apposito spazio di memoria riservato ad un programma; si ha la possibilità di generare molteplici thread autonomi.
In un certo senso si può affermare che ,in questo caso, la JVM simula il comportamento del microprocessore. Per gestire un thread bisogna:

1 . dichiarare una classe che descriva il comportamento del thread.
2 . controllare il ciclo di vita del thread richiamato dal programma principale.

Per realizzare il punto 1 si dichiara il thread tramite la seguente classe:

class nomeThread extends thread |or implements runnable {
// dichiarazione attributi e metodi della classe
public void run( ) {
//elaborazione eseguita dal thread
}

In genere si crea la versione con l'interfaccia runnable dato che la versione extend thread è considerata deprecata (ma non è il caso di formalizzarsi per il momento).

Per realizzare il punto 2 si istanzia il thread tramite la seguente sintassi:

nomeThread nomeOggetto = new nomeThread(parametri);

In pratica, per creare due processi che operano parallelamente, possono bastare le seguenti due classi.

public class esempio1 {//punto 2
public static void main(String [] args) {
A T1 = new A();
B T2 = new B();
T1.start();
T2.start();
}//fine main
}//fine class
class A extends Thread {//punto 1
public void run() {
for (;;)System.out.println("A");
}//fine run
}//fine A
class B extends Thread {//punto 1
public void run() {
for (;;) System.out.println("B");
}//fine run
}//fine B

Nel precedente esempio il main() attiva due thread (T1,T2) che concorrono fra loro per l'accesso allo schermo, il processo A stampa 'A', il processo B stampa 'B'.
Il programma può essere modificato in modo che i due processi possano essere istanziati con la stessa sottoclasse thread (P) come si nota nell'esempio seguente.

public class esempio2 {// punto 2
public static void main(String [] args) {
P T1 = new P('A');
P T2 = new P('B');
T1.start();T2.start();
}//fine main
}//fine class
class P extends Thread {// punto 1
private char ch;
public P(char c) {ch = c;}//costruttore
public void run() {
for (;;) System.out.println(ch);
}//fine run
}//fine classe P

Possiamo poi 'temporizzare' l'esecuzione dei due processi eseguiti in parallelo tramite un blocco try-catch prima dei comandi di stop() ci permette di azionare i due processi A e B in parallelo per 5000ms=5sec.

class esempio3{
public static void main(String args[]){
P T1=new P('A');
P T2=new P('B');
T1.start(); T2.start();
try{ Thread.sleep(5000);
}catch(InterruptedException e){e.printStackTrace();}
T1.stop(); T2.stop();
}//fine main
}//fine classe esempio3
class P extends Thread{
char ch;
public P(char c){ ch=c; }//fine costruttore
public void run(){
while(true) System.out.println(ch);
}//fine run
}//fine classe P

Il thread nasce quando viene invocato col metodo start() rimane in esecuzione, svolgendo le istruzioni contenute nel metodo run() che può essere sospeso col metodo sleep(n) con n intervallo di tempo della sospensione anche dall'interno del metodo run(). Il thread termina quando si conclude il programma chiamante oppure quando viene invocato il metodo stop().

Il programma funziona egregiamente ma il compilatore ci avvisa che l'uso del metodo stop() è ormai considerato deprecato, perché si possono verificare situazioni in cui l'oggetto generato può entrare in uno stato di inconsistenza. In pratica è consigliato l'utilizzo di variabili 'osservabili' per la terminazione di un thread. E' quindi possibile scrivere una versione non deprecabile del programma precedente:

class esempio4{
public static void main(String args[]){
P P1=new P('A');
P P2=new P('B');
Thread T1=new Thread(P1);
Thread T2=new Thread(P2);
T1.start(); T2.start();
try{ Thread.sleep(5000);
}catch(InterruptedException e){e.printStackTrace();}
P1.ferma(); P2.ferma();
}//fine main
}//fine classe esempio4
class P implements Runnable{
char ch;
boolean stop=false;//variabile di controllo
public P(char c){ch=c;}//fine costruttore
public void run(){
while(!stop) System.out.println(ch);
}//fine run
public void ferma(){stop=true;}
}//fine classe P

Ma cosa vuol dire che un oggetto può entrare in stato di inconsistenza? Supponiamo che due titolari di un conto corrente possano depositare o prelevare denaro servendosi anche simultaneamente da sportelli bancari diversi. Supponiamo che l'oggetto 'conto' abbia nella sua variabile totale il valore 2000. Ci si aspetta che se qualcuno deposita 500 e qualcun altro deposita 800, e supponendo che le tasse per ogni versamento siano 100, alla fine la variabile totale_conto valga 3100. Supponiamo che questi due depositi vengano fatti in concorrenza da due thread diversi e che durante l'esecuzione i thread si alternino sul processore, è possibile il verificarsi della seguente situazione:

Processo A Processo B
conto.versamento(1000)
// il thread invoca il metodo e il flusso di esecuzione fa a eseguire le istruzioni di tale metodo
 
  conto.versamento(500)
// il thread invoca il metodo e il flusso di esecuzione fa a eseguire le istruzioni di tale metodo
totale=totale+importo-tasse;
//nuovo_totale = 2000+800-100
 
  nuovo=totale_conto+importo-tasse;
//nuovo_totale = 2000+500-100
totale=2700;  
  totale=2400;

invece di 3100 il nuovo totale potrebbe essere di 2400! In pratica, quando si attivano i thread, non è possibile conoscere a priori la sequenza delle operazioni che avverranno e i dati possono essere in questo modo, facilmente corrotti.
Normalmente i thread di un'applicazione non sono indipendenti come i due processi dell'esempio precedente. Piuttosto essi concorrono al raggiungimento di un obiettivo globale e necessitano di comunicazione e cooperazione.

Sincronizzazione  
    

Nella programmazione multiprocesso i metodi che possono essere richiamati simultaneamente da un thread possono essere dichiarati con il modificatore synchronized. Per le istruzioni sincronizzate, si usa la forma:

synchronized(oggetto) {
//.. codice sincronizzato
}

Per i metodi sincronizzati, si usa la forma:

synchronized tipoRestituitoDalMetodo nomeMetodo(parametri) {
//.. codice sincronizzato
}

il modificatore synchronized realizza un accesso esclusivo, controllato da un oggetto di sistema chiamato monitor (o semaforo) assicurando che un solo thread alla volta possa eseguire il blocco di codice dichiarato nel metodo. Un esempio è il seguente:

class cc{
public static void main(String args[]){
monitor conto =new monitor(1000);
cliente T1=new cliente(conto);
cliente T2=new cliente(conto);
T1.start();T2.start();
try{ Thread.sleep(15000);//durata totale=15s
}catch(InterruptedException e){e.printStackTrace();}
T1.stop();T2.stop();
}//fine main
}//fine classe cc
class monitor{
protected int saldo;
monitor(int saldoIniziale){saldo=saldoIniziale;}//costruttore
synchronized int getSaldo(){return saldo;}
synchronized void deposito(int somma){ saldo=saldo+somma;}
synchronized void prelievo(int somma){ saldo=saldo-somma;}
}//fine monitor
class cliente extends Thread{
monitor conto;
int saldo cliente(monitor c){conto=c;}//fine costruttore
public void run(){
while(true){
try{//tempo di latenza del processo=1sec
this.sleep(1000);
}catch(InterruptedException e){e.printStackTrace();}
int somma=500+(int)(Math.random()*200);
if(Math.random()<0.5) synchronized(conto){
   System.out.print(this.getName());
   conto.deposito(somma);
   saldo=conto.getSaldo();
   System.out.println(" depositati:"+somma+" saldo:"+saldo);
} else synchronized(conto){
   System.out.print(this.getName());
   conto.prelievo(somma);
   saldo=conto.getSaldo();
   System.out.println(" prelevati:"+somma+" saldo:"+saldo);
}//synchronized
}//fine while
}//fine run
}//fine classe cliente

Il cui output puà essere il seguente

- - -
Thread-0 prelevati:502 saldo:498
Thread-1 prelevati:608 saldo:-110
Thread-0 depositati:641 saldo:531
Thread-1 prelevati:590 saldo:-59
Thread-0 depositati:679 saldo:620
Thread-1 prelevati:671 saldo:-51
Thread-0 prelevati:541 saldo:-592
Thread-1 prelevati:644 saldo:-1236
- - -

In questo modo i due clienti si contendono la risorsa 'saldo'.
Questa competizione viene regolata dai meccanismi di sincronizzazione che impediscono ad un processo di accedere ad una risorsa mentre questa è in utilizzo ad un processo concorrente. Avremmo potuto scrivere una classe 'conto' come:

class conto{
private int saldo;
conto(int saldoIniziale){saldo=saldoIniziale;}
int getSaldo(){return saldo;}
void deposito(int importo){
saldo=saldo+importo;
}

void prelievo(int importo){ saldo=saldo-importo;}
}//fine conto

Istanziando due oggetti concorrenti C1 e C2.
Ma se durante una operazione concorrente i due clienti avessero attivato due istruzioni del tipo:

C1.deposito(500)        C2.deposito(200)

avremmo potuto perdere uno dei due depositi.
Nel nostro caso, invece, si crea un oggetto 'conto' come istanza della classe 'monitor' dotata di metodi sincronizzati e la si fa condividere ai clienti passandola come costruttore allo stesso thread dei clienti. I metodi sincronizzati sono nella classe monitor, le istruzioni sincronizzate, nella classe clienti. Per noi, ogni cliente userà una forma :

synchronized(conto){ conto.deposito(importo);}

La classe monitor, viene da noi usata come concentratore delle operazioni che possono essere usate in modo unitario su un oggetto, senza che vi siano interferenze da parte di un altro oggetto. Per verificare riportiamo la versione non sincronizzata del programma.

class ccns{
public static void main(String args[]){ //versione non sincronizzata
conto C1=new conto(1000);
conto C2=new conto(1000);
C1.start();C2.start();
try{ Thread.sleep(15000);//durata totale=15s
}catch(InterruptedException e){e.printStackTrace();}
C1.stop();C2.stop();
}//fine main
}//fine classe ccns
class conto extends Thread{
private int saldo;
conto(int saldoIniziale){saldo=saldoIniziale;}//costruttore
public void run(){
while(true){
   try{this.sleep(1000);
   }catch(InterruptedException e){e.printStackTrace();}
   int somma=500+(int)(Math.random()*200);
   if(Math.random()<0.5){
      System.out.print(this.getName());
      deposito(somma);
      int saldo=getSaldo();
      System.out.println(" depositati:"+somma+" saldo:"+saldo);
   }else{
      System.out.print(this.getName());
      prelievo(somma);
      int saldo=getSaldo();
      System.out.println(" prelevati:"+somma+" saldo:"+saldo);
   }//fine if
}//fine while
}//fine run
//metodi
int getSaldo(){return saldo;}
void deposito(int somma){saldo=saldo+somma;}
void prelievo(int somma){saldo=saldo-somma;}
}//fine classe conto

Concludendo:in presenza di sincronizzazione si pone un lock (blocco) su un oggetto. Quando un oggetto ha un lock da parte di qualche thread solo tale thread può accedere ad esso.

Se un thread invoca un metodo synchronyzed su di un oggetto, l'oggetto viene bloccato, ha cioè un lock. Un altro thread che invochi un metodo synchronyzed sullo stesso oggetto rimarrà bloccato fino a quando il lock non sarà rimosso.

Il meccanismo di blocco synchronyzed è sufficiente per evitare interferenze fra i thread, tuttavia è anche necessario disporre di uno strumento che permetta la comunicazione fra i thread. A tale scopo è stato definito il metodo wait, che mette un thread in attesa fino a quando non si verifichi una determinata condizione, e il metodo notify, utile per comunicare ai thread in attesa che qualcosa è accaduto.

Wait , Notify & deadlock      

Come si è notato dagli esempi precedenti, il saldo può diventare negativo. Se ipotizziamo che questa condizione non sia permessa possiamo mettere il cliente in 'attesa' che il valore del conto sia sufficiente per soddisfare il suo prelievo. La tecnica consiste nel mettere in attesa il processo su di una determinata condizione attraverso la forma:

while(condizione)
      try{wait();}catch(InterruptedException e){e.printStackTrace();}

wait fa parte di un insieme di comandi della classe Object (disponibile quindi per ogni classe) ideato per mettere in attesa un thread.

wait() - attende che qualche altro thread che usa questo stesso oggetto condiviso esegua una notify() o una notifyAll().

notify() - risveglia un thread che si è messo in attesa su questo stesso oggetto condiviso (nel nostro caso l'oggetto conto istanziato dalla classe monitor), con un wait().

notifyAll() - risveglia tutti i thread che si sono messi in attesa su questo oggetto condiviso.

Questi metodi permettono ad un thread di sospendersi all'interno di un monitor (wait), e di risvegliare uno (notify) o tutti (notifyAll) i thread sospesi.

Se volessimo implementare questi metodi nella versione sincronizzata del nostro programma basterebbe modificare i due metodi della classe monitor così:

synchronized void deposito(int somma){
saldo+=somma; notifyAll();
}
synchronized void prelievo(int somma,String nome){
while(somma>saldo){
System.out.println(nome+" INSUFFICIENTE per:"+somma);
try{
wait();
}catch(InterruptedException e){e.printStackTrace();}
}//fine while
saldo=saldo-somma;
}

Attenzione! Per testare questa versione possiamo inizializzare il nostro conto a 100 con l'istruzione:

monitor conto =new monitor(100);

L'output potrebbe essere il seguente:

- - -
Thread-0 INSUFFICIENTE per:547
Thread-1 depositati:566 saldo:666
Thread-0 prelevati:547 saldo:119
Thread-0 depositati:605 saldo:724
Thread-1 depositati:523 saldo:1247
Thread-0 depositati:610 saldo:1857
Thread-1 depositati:606 saldo:2463
Thread-0 prelevati:606 saldo:1857
Thread-1 prelevati:638 saldo:1219
Thread-0 depositati:530 saldo:1749
Thread-1 depositati:630 saldo:2379
Thread-0 prelevati:501 saldo:1878
Thread-1 prelevati:699 saldo:1179
Thread-0 prelevati:531 saldo:648
Thread-1 prelevati:526 saldo:122
Thread-0 INSUFFICIENTE per:640
Thread-1 INSUFFICIENTE per:630
- - -

Cioè tutto fila liscio finché uno dei thread fa una richieste superiori alla disponibilità del saldo, a quel punto viene messo in attesa, se l'altro thread esegue un deposito sufficiente a soddisfare le richieste di prelievo del primo le transazioni procedono perché al primo processo viene notificata (notify) la disponibilità della sua richiesta. Se sia il primo che il secondo thread fanno richieste superiori alla disponibilità del saldo entrambi i processi vanno in attesa e si ha dunque una situazione di stallo (deadlock).

Esistono tecniche che permettono di uscire da una situazione di stallo ma spesso, come in questo caso, è meglio prevenire tale condizione. Nel codice sincronizzato della classe cliente modifichiamo il ramo else:

else synchronized(conto){
   if(somma < saldo){
      conto.prelievo(somma,this.getName());
      saldo=conto.getSaldo();
      System.out.println(this.getName()+" prelevati:"+somma+" saldo:"+saldo);
   }//fine if
}//synchronized

Schedulazione      

Le tre primitive :wait(),notify e notifyAll() possono solo essere chiamate se il processo che le invoca possiede in quel momento il monitor. Bisogna specificare che di per se, il main() di un programma costituisce un thread.
In questo programma, ad esempio, viene istanziato un processo dormiente; il monitor (oggetto condiviso) è una sveglia temporizzata.
Il main() esegue la notify() all'istruzione

s.risveglia();

di fatto il main() occupa il monitor (sveglia) prima che il processo dormiente esegua la wait() ed esegue il notify() in modo prematuro. Il wait() viene invocata solo successivamente dall'oggetto dormiente che rimane in attesa permanente.

class wn{
public static void main(String[] args) {
Sveglia s = new Sveglia();
Dormiente d = new Dormiente(s,3000);
//d.setPriority (Thread.MAX_PRIORITY);
d.start();
synchronized (s) {
    s.risveglia();
    System.out.println(" Svegliati !");
}//fine synchronized
}//fine main
}//fine class wn

class Sveglia { //monitor
private int ritardo;
synchronized void Sonno(int quanto){
   ritardo = quanto;
   System.out.println("Sveglia tra: " + ritardo/1000 + " secondi");
   try {
   wait();
   }catch(InterruptedException e) {e.printStackTrace();}
}//fine synchronized
synchronized void risveglia() {
   try {
   Thread.sleep(ritardo);
   }catch(InterruptedException e) {e.printStackTrace();}
   notify();
System.out.println(" notificata sveglia"); }
} //fine synchronized

class Dormiente extends Thread {
Sveglia s;
int durataSonno;
Dormiente(Sveglia s, int durataSonno ) {//costruttore
this.s = s;
this.durataSonno = durataSonno;
} //fine costruttore
public void run() {
   s.Sonno(durataSonno);
   System.out.println("Ora sono sveglio.");
} //fine run
} //fine classe dormiente

Il programma rimane, dunque, in uno stato di blocco (sonno) permanente. L'introduzione dell'istruzione:

d.setPriority(Thread.MAX_PRIORITY);

risolve questa condizione di blocco, perchè permette al processo dormiente di subentrare al main() subito dopo che il main() crea e possiede la sveglia 's', a livello dell'istruzione:

d.start();

Le tre costanti:

MAX_PRIORITY
NORM_PRIORITY
MIN_PRIORITY

permettono di dare ad un oggetto thread una priorità e consentono di fatto una politica di schedulazione basata su una attribuzione di precedenze.