Java/Multithreading: differenze tra le versioni

Wikibooks, manuali e libri di testo liberi.
Contenuto cancellato Contenuto aggiunto
Ramac (discussione | contributi)
m fix
Sumail (discussione | contributi)
capitolo rifatto e quasi ultimato
Riga 1: Riga 1:
{{java}}
{{java}}
Sorge spesso l'esigenza di fare eseguire al programma più operazioni in parallelo, ossia contemporaneamente. Per esempio si potrebbe voler salvare sul disco un flusso di dati ricevuti da internet mentre si inviano altri dati a un altra macchina o si gestisce un' interfaccia grafica.
I moderni computer sono in grado di eseguire più di una operazione alla volta, ad esempio potete stampare il vostro curriculum mentre controllate la posta o ascoltare musica mentre navigate in Internet. A livello più basso questo si traduce nell'esecuzione simultanea di più funzioni, e ognuna di queste operazioni parallele è detto '''''thread'''''.
Poiché il linguaggio, per quanto abbiamo visto finora, viene eseguito una sola istruzione alla volta in maniera lineare, è necessario introdurre una nuova struttura che permetta di "sdoppiare" il flusso di esecuzione del programma per compiere più operazioni simultaneamente. Questa struttura è il '''thread'''.
È bene evidenziare come la [[w:CPU|CPU]], in realtà, esegua comunque una sola operazione alla volta mentre è il [[w:sistema operativo|sistema operativo]] a alternare ciclicamente l'esecuzione dei vari processi così velocemente da far sembrare all'utente che questi stiano venendo eseguiti simultaneamente. Nel caso di un computer dotato di più processori, invece, può esserci effettiva simultaneità ma solo per tanti processi quante sono le CPU.

Può essere utile utilizzare i thread anche per eseguire più velocemente molte operazioni di I/O: poiché queste portano il thread a fermarsi fino all'avvenuta operazione (e a passare le risorse a un altro thread o processo), utilizzando più thread è possibile sfruttare i tempi di attesa per richiedere nuove operazioni I/O.

Per usare un thread si deve creare una classe che estenda la classe Thread contenente il codice da eseguire in parallelo e in seguito richiamarne il metodo '''run()'''. Questo metodo ritorna immediatamente ma avvia il codice contenuto dentro la classe, che verrà eseguito in cimultanea al programma chiamante. Naturalmente è possibile aprire vari thread alla volta e un thread può, se lo vogliamo, aprirne altri.

Creo una classe ''Chiamato'' che estende Thread:


Java implementa i ''thread'' come superclassi: è sufficiente derivare la classe dalla superclasse '''Thread''', che definisce un metodo <tt>run()</tt>:
<source lang="java">
<source lang="java">
class Processo extends Thread
class Chiamato extends Thread
{ public void run()
{ public void run(){
{ ...
while(true){
System.out.print("sono il thread");
}
public int parlami()
}
{ ...
}
}
}
</source>

Poi, eseguo il thread.
<source lang="java">
public static void main(String argc[]){
Chiamato p = new Chiamato();
System.out.println("eseguo il thread");
p.run();

while(true){
System.out.print("sono il chiamante");
}
}
}
</source>
</source>

Poi, creare un oggetto di quella classe ed eseguirlo:
Al contrario del solito, la funzione run ritorna subito, ma il processo (l'oggetto istanziato) resta attivo.

Il risultato sarà una produzione continua di messaggi "sono il thread" e "sono il chiamante", che si alterneranno man mano che il sistema operativo e la macchina virtuale alterneranno l'esecuzione tra i thread.

I thread possono interagire tra loro come fanno delle classi qualsiasi: un thread è comunque una classe, può ricevere e memorizzare reference dal costruttore, se ne possono chiamare i metodi definiti all'interno e anche il metodo run() può chiamare metodi dei vari oggetti.

Il fatto che i thread siano eseguiti in simultanea dà origine a un nuovo tipo di problema, detto ''di concorrenza'': cosa accade se un thread modifica un valore di una variabile mentre un altro lo richiede?

Esistono scenari ancora più complessi che possono essere compresi con un esempio.

Abbiamo creato un videogioco di ruolo multigiocatore la cui grafica è gestita da una classe ''Grafica'' che estende ''Thread'' e si occupa di muovere a piccoli passi le immagini dei personaggi dando l'illusione del movimento continuo.Ovviamente il gioco ha un suo thread in modo da poter reagire alla pressione dei tasti e ricevere informazioni dagli altri computer connessi mentre la grafica gestisce le animazioni indipendentemente.
Il gioco chiama il metodo muovi("elfo","nord") che sposta il personaggio elfo a nord gestendone l'animazione, che è gestita in parallelo mentre il gioco continua il suo corso. Se si riceve l'ordine (dalla tastiera) di muovere un personaggio, viene chiamato di nuovo il metodo ''muovi''. La grafica memorizzerà internamente la direzione in cui muovere il personaggio e un int che indica il numero di fotogrammi dell'animazione già mostrati. Ad ogni passo di un ciclo while viene mostrato il fotogramma dell'animazione adatto e il numero viene incrementato. Quando arriva alla fine il ciclo si ferma.

'''Problema''': cosa accade se il gioco richiede un nuovo movimento mentre la grafica sta già eseguendo un'animazione ?
*La nuova richiesta viene ignorata?
*L'animazione già avviata viene interrotta a metà per avviare quella nuova ?
*Le due animazioni vengono "fuse" in maniera imprevedibile?
*La nuova animazione viene accodata per essere eseguita quando possibile ?

Non è possibile saperlo a priori, e l'aspetto peggiore del problema è che può manifestarsi solo a volte, rendendo difficile individuarlo e quindi correggerlo.

Per evitare questi problemi Java offre molti strumenti.

===Metodi synchronized===
Innanzitutto è possibile indicare che un metodo non può essere invocato simultaneamente da più thread con la parola chiave '''synchronized'''
<source lang="java">
<source lang="java">
synchronized void muovi(String nome,String direzione){
Processo p = new Processo();
...
p.run();
}
</source>
</source>
Al contrario del solito, la funzione run ritorna subito, ma il processo (l'oggetto istanziato) resta attivo. Per comunicare con il processo, potete usare la variabile (''handle'', in gergo threadesco) per eseguire uno qualsiasi dei suoi metodi:
<source lang=java>numeri_vincenti = p.parlami();</source>


in questo modo se un thread chiama il metodo quando questo sta già venendo eseguito da un altro thread viene messo in attesa finché l'esecuzione termina.
Bisogna usare synchronized tutte le volte che un metodo che manipola dei dati potrebbe dare problemi se fosse eseguito in più istanze contemporaneamente.
Per esempio, un metodo che ordina dei dati in una lista scambiandoli tra di loro a due a due ([[w:bubblesort|bubblesort]]) potrebbe scambiare dei dati già scambiati dall'altra istanza ottenendo dei risultati imprevedibili.

Un metodo che si limita a leggere dei dati e restituirli al chiamante, invece, non necessita quasi mai di essere controllato in questo modo

==Campi volatili==
Un'operazione che non può essere scomposta in operazioni più semplici è detta ''atomica''. Un assegnamento o la lettura di una variabile di tipo ''boolean'' sono considerati operazioni atomiche, e ciò è molto utile perché non potendo essere interrotte per il passaggio a un altro thread possonno fungere da '''semafori''' utili a coordinare i thread di un programma.

La JVM però a volte ottimizza l'esecuzione dei programmi multithread copiano copie temporanee delle variabili per i vari thread.
Questo fa sì che il programmatore creda di avere modificato una variabile ma alcuni thread non ne vedono immediatamente il valore cambiato.
Per disattivare questo meccanismo bisogna utilizzare la parola chiave '''volatile''', che indica alla JVM di non ottimizzarla rendendone la manipolazione effettivamente atomica.
<source lang="java">
public volatile boolean pronto=true;
</source>
[[Categoria:java|Threads]]
[[Categoria:java|Threads]]
{{Avanzamento|25%|1 luglio 2009}}
{{Avanzamento|75%|6 giugno 2010}}

Versione delle 23:23, 6 giu 2010

Indice del libro

Sorge spesso l'esigenza di fare eseguire al programma più operazioni in parallelo, ossia contemporaneamente. Per esempio si potrebbe voler salvare sul disco un flusso di dati ricevuti da internet mentre si inviano altri dati a un altra macchina o si gestisce un' interfaccia grafica. Poiché il linguaggio, per quanto abbiamo visto finora, viene eseguito una sola istruzione alla volta in maniera lineare, è necessario introdurre una nuova struttura che permetta di "sdoppiare" il flusso di esecuzione del programma per compiere più operazioni simultaneamente. Questa struttura è il thread. È bene evidenziare come la CPU, in realtà, esegua comunque una sola operazione alla volta mentre è il sistema operativo a alternare ciclicamente l'esecuzione dei vari processi così velocemente da far sembrare all'utente che questi stiano venendo eseguiti simultaneamente. Nel caso di un computer dotato di più processori, invece, può esserci effettiva simultaneità ma solo per tanti processi quante sono le CPU.

Può essere utile utilizzare i thread anche per eseguire più velocemente molte operazioni di I/O: poiché queste portano il thread a fermarsi fino all'avvenuta operazione (e a passare le risorse a un altro thread o processo), utilizzando più thread è possibile sfruttare i tempi di attesa per richiedere nuove operazioni I/O.

Per usare un thread si deve creare una classe che estenda la classe Thread contenente il codice da eseguire in parallelo e in seguito richiamarne il metodo run(). Questo metodo ritorna immediatamente ma avvia il codice contenuto dentro la classe, che verrà eseguito in cimultanea al programma chiamante. Naturalmente è possibile aprire vari thread alla volta e un thread può, se lo vogliamo, aprirne altri.

Creo una classe Chiamato che estende Thread:

class Chiamato extends Thread
{  public void run(){
     while(true){
      System.out.print("sono il thread");
     }
   }
 }

Poi, eseguo il thread.

public static void main(String argc[]){
     Chiamato p = new Chiamato();
     System.out.println("eseguo il thread");
     p.run();

     while(true){
      System.out.print("sono il chiamante");
     }
}

Al contrario del solito, la funzione run ritorna subito, ma il processo (l'oggetto istanziato) resta attivo.

Il risultato sarà una produzione continua di messaggi "sono il thread" e "sono il chiamante", che si alterneranno man mano che il sistema operativo e la macchina virtuale alterneranno l'esecuzione tra i thread.

I thread possono interagire tra loro come fanno delle classi qualsiasi: un thread è comunque una classe, può ricevere e memorizzare reference dal costruttore, se ne possono chiamare i metodi definiti all'interno e anche il metodo run() può chiamare metodi dei vari oggetti.

Il fatto che i thread siano eseguiti in simultanea dà origine a un nuovo tipo di problema, detto di concorrenza: cosa accade se un thread modifica un valore di una variabile mentre un altro lo richiede?

Esistono scenari ancora più complessi che possono essere compresi con un esempio.

Abbiamo creato un videogioco di ruolo multigiocatore la cui grafica è gestita da una classe Grafica che estende Thread e si occupa di muovere a piccoli passi le immagini dei personaggi dando l'illusione del movimento continuo.Ovviamente il gioco ha un suo thread in modo da poter reagire alla pressione dei tasti e ricevere informazioni dagli altri computer connessi mentre la grafica gestisce le animazioni indipendentemente. Il gioco chiama il metodo muovi("elfo","nord") che sposta il personaggio elfo a nord gestendone l'animazione, che è gestita in parallelo mentre il gioco continua il suo corso. Se si riceve l'ordine (dalla tastiera) di muovere un personaggio, viene chiamato di nuovo il metodo muovi. La grafica memorizzerà internamente la direzione in cui muovere il personaggio e un int che indica il numero di fotogrammi dell'animazione già mostrati. Ad ogni passo di un ciclo while viene mostrato il fotogramma dell'animazione adatto e il numero viene incrementato. Quando arriva alla fine il ciclo si ferma.

Problema: cosa accade se il gioco richiede un nuovo movimento mentre la grafica sta già eseguendo un'animazione ?

  • La nuova richiesta viene ignorata?
  • L'animazione già avviata viene interrotta a metà per avviare quella nuova ?
  • Le due animazioni vengono "fuse" in maniera imprevedibile?
  • La nuova animazione viene accodata per essere eseguita quando possibile ?

Non è possibile saperlo a priori, e l'aspetto peggiore del problema è che può manifestarsi solo a volte, rendendo difficile individuarlo e quindi correggerlo.

Per evitare questi problemi Java offre molti strumenti.

Metodi synchronized

Innanzitutto è possibile indicare che un metodo non può essere invocato simultaneamente da più thread con la parola chiave synchronized

synchronized void muovi(String nome,String direzione){
...
}

in questo modo se un thread chiama il metodo quando questo sta già venendo eseguito da un altro thread viene messo in attesa finché l'esecuzione termina. Bisogna usare synchronized tutte le volte che un metodo che manipola dei dati potrebbe dare problemi se fosse eseguito in più istanze contemporaneamente. Per esempio, un metodo che ordina dei dati in una lista scambiandoli tra di loro a due a due (bubblesort) potrebbe scambiare dei dati già scambiati dall'altra istanza ottenendo dei risultati imprevedibili.

Un metodo che si limita a leggere dei dati e restituirli al chiamante, invece, non necessita quasi mai di essere controllato in questo modo

Campi volatili

Un'operazione che non può essere scomposta in operazioni più semplici è detta atomica. Un assegnamento o la lettura di una variabile di tipo boolean sono considerati operazioni atomiche, e ciò è molto utile perché non potendo essere interrotte per il passaggio a un altro thread possonno fungere da semafori utili a coordinare i thread di un programma.

La JVM però a volte ottimizza l'esecuzione dei programmi multithread copiano copie temporanee delle variabili per i vari thread. Questo fa sì che il programmatore creda di avere modificato una variabile ma alcuni thread non ne vedono immediatamente il valore cambiato. Per disattivare questo meccanismo bisogna utilizzare la parola chiave volatile, che indica alla JVM di non ottimizzarla rendendone la manipolazione effettivamente atomica.

public volatile boolean pronto=true;