Java/Multithreading

Indice del libro

Introduzione

modifica

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 simultanea 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.start();

     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.

Sincronizzazione

modifica

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?

Ad esempio, un Thread potrebbe ordinare i valori in un array numerico tramite un bubblesort mentre un altro lo legge un valore alla volta, e quest'ultimo potrebbe leggere due volte lo stesso valore perché è stato spostato durante la lettura dal thread concorrente.

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

modifica

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 perché le letture di dati non danno problemi di concorrenza tra di loro.

Campi volatili

modifica

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;