Thread.sleep(ms): pausa,
può lanciare
InterruptedException
Dati condivisi
campi statici e di istanza:
condivisi
variabili locali: una per
invocazione
lettura prima di scrivere:
valore vecchio
Interfaccia grafica
ciclo degli eventi: attende,
richiama gli ascoltatori,
ridisegna
l'ascoltatore gira nel
thread dell'interfaccia
ascoltatore lungo: finestra
bloccata, eventi in coda
operazione lunga: in un
thread a parte
Più flussi di esecuzione
Di norma un programma esegue le istruzioni di main una dopo l'altra. Alcune applicazioni devono però seguire più attività insieme: un server che gestisce più connessioni, un'interfaccia grafica che reagisce agli eventi mentre fa altro. Servono più flussi di esecuzione paralleli, cioè più thread.
Creare un thread è semplice. La difficoltà viene dopo, quando i flussi vanno sincronizzati.
In Java le istruzioni stanno nei metodi e i metodi nelle classi. Per dire quali istruzioni eseguire in un flusso a parte si scrive quindi una classe che implementa Runnable e le si mette nel metodo run(). L'interfaccia lo dichiara astratto, quindi va definito per forza.
public class Parallelo implements Runnable { public void run() { System.out.println("parallelo"); }}
Creare un oggetto di questa classe non avvia niente: p.run() eseguirebbe il metodo nel flusso di main, in sequenza. Per eseguirlo in un flusso a parte si passa l'oggetto al costruttore di Thread, che non va importato, e si chiama start().
public class Principale { public static void main(String[] args) { Parallelo p = new Parallelo(); Thread t = new Thread(p); t.start(); System.out.println("main"); }}
Dopo start() il flusso di main e quello di run() avanzano insieme. Il Runnable raccoglie il lavoro in un oggetto, che si può passare al costruttore di Thread.
L'ordine non si può prevedere
«In parallelo» non vuol dire una lettera o una riga per volta. Il gestore dei thread concede un po' di tempo a un flusso, poi a un altro, quindi l'alternanza avviene per blocchi di istruzioni: cambiare flusso a ogni istruzione costerebbe troppo. Non si può sapere quando avviene il passaggio. Un thread può girare a lungo prima di cedere, oppure poco, e il programma deve funzionare in ogni caso.
Per vedere l'alternanza si rallentano i flussi con Thread.sleep(ms), che sospende il thread per ms millisecondi. In quel tempo il gestore ne esegue un altro. Con pause casuali due cicli di stampe si intercalano in modo diverso a ogni esecuzione. Anche con pause uguali, ogni tanto uno dei due fa due iterazioni di seguito.
sleep può lanciare InterruptedException. Il metodo run() di Runnable non la dichiara, quindi nell'implementazione non si può scrivere throws InterruptedException: si gestisce con try e catch. Per non ripetere il blocco lo si raccoglie in un metodo statico.
public class Principale { static void sleep(long millisecondi) { try { Thread.sleep(millisecondi); } catch (InterruptedException e) { System.out.println(e); } } public static void main(String[] args) { Thread t = new Thread(new Parallelo()); t.start(); for (int i = 0; i < 20; i++) { System.out.println("main " + i); sleep((long) (100 * Math.random())); } }}
public class Parallelo implements Runnable { public void run() { for (int i = 0; i < 20; i++) { System.out.println("parallelo " + i); Principale.sleep((long) (100 * Math.random())); } }}
Campi condivisi
I flussi condividono i campi: un campo statico è una sola variabile della classe, un campo di istanza appartiene all'oggetto passato al thread, e chi raggiunge la stessa classe o lo stesso oggetto vede lo stesso valore. Le variabili locali di un metodo no, perché ogni invocazione ha le sue.
Nell'esempio main assegna 1 a valore, avvia il thread, aspetta 100 millisecondi e stampa valore. Il metodo run() assegna 2.
public class Parallelo implements Runnable { public void run() { Principale.valore = 2; }}
public class Principale { static int valore; public static void main(String[] args) throws InterruptedException { valore = 1; Thread t = new Thread(new Parallelo()); t.start(); Thread.sleep(100); System.out.println(valore); }}
Di solito il programma stampa 2: la variabile è condivisa, e in 100 ms il thread ha quasi sempre avuto il tempo di scriverla. L'attesa però non garantisce l'ordine. Il risultato dipende da chi arriva prima: se main stampa prima che il thread abbia scritto, vede ancora 1, e senza l'attesa succede facilmente. È il problema della sincronizzazione: gli accessi condivisi hanno bisogno di meccanismi per coordinare i flussi.
Processi e thread
Un processo ha una memoria tutta sua. Se un processo ne crea un altro, nel nuovo c'è una copia di ogni variabile: ha lo stesso nome, ma è un'altra variabile. Condividere dati fra processi richiede di predisporre una memoria condivisa. I thread nascono come «processi leggeri» proprio perché lavorano nella memoria dello stesso processo, senza duplicarla. I sistemi operativi attuali, comunque, copiano la memoria di un processo solo quando viene modificata (copia su scrittura).
Aspettare il risultato con join()
Come si passa un dato a run() e si recupera il risultato senza variabili statiche? Serve poter avviare più esecuzioni, e una sola variabile statica sarebbe comune a tutte. Il punto comune sono i campi dell'oggetto Runnable, come per gli ascoltatori. main scrive input. run() aspetta un secondo, con il metodo sleep di Principale, e scrive in output il doppio.
public class Raddoppia implements Runnable { int input; int output; public void run() { Principale.sleep(1000); this.output = 2 * this.input; }}
Con r.run() il calcolo avviene nel flusso di main, che aspetta la fine e poi legge 10. Con start() il main prosegue subito: se legge output prima che il thread abbia scritto, trova ancora il valore iniziale 0.
Raddoppia r = new Raddoppia();r.input = 5;Thread t = new Thread(r);t.start();System.out.println(r.output);
Un'attesa con sleep scelta a priori non risolve il problema. Il thread può impiegare un tempo imprevedibile, per esempio se usa la rete o legge un file. Serve aspettare la sua conclusione: lo fa join().
public static void main(String[] args) throws InterruptedException { Raddoppia r = new Raddoppia(); r.input = 5; Thread t = new Thread(r); t.start(); t.join(); System.out.println(r.output);}
t.join() sospende il thread che lo chiama finché t non termina, quindi la stampa dà 10. Anche join() può lanciare InterruptedException, e qui main la dichiara con throws. Il nome fa pensare che i flussi si riuniscano, ma il metodo si limita ad aspettare. Finché output non serve i flussi proseguono insieme: appena serve, si chiama prima join().
Un conteggio nell'interfaccia grafica
Si vuole una finestra con un'etichetta e due pulsanti, «parti» e «ferma», come un cronometro. «Parti» fa contare l'etichetta 1, 2, 3, … ogni decimo di secondo, senza fine. «Ferma» dovrebbe interrompere il conteggio.
La finestra estende JFrame e usa un GridLayout con GridLayout(0, 1): una sola colonna, quindi i componenti stanno uno sopra l'altro senza bisogno di un pannello.
import java.awt.GridLayout;import javax.swing.JButton;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.SwingConstants;public class Finestra extends JFrame { public Finestra() { super("Conteggio"); this.setLayout(new GridLayout(0, 1)); JLabel etichetta = new JLabel("0", SwingConstants.CENTER); JButton parti = new JButton("parti"); JButton ferma = new JButton("ferma"); Ascoltatore ascoltatore = new Ascoltatore(etichetta); parti.addActionListener(ascoltatore); ferma.addActionListener(ascoltatore); this.add(etichetta); this.add(parti); this.add(ferma); this.pack(); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setVisible(true); }}
Senza ascoltatori i pulsanti non fanno niente. Un solo ascoltatore serve entrambi, e getActionCommand() restituisce il testo del pulsante premuto. Il conteggio è il corpo di run() in una classe Conta, che riceve l'etichetta.
import javax.swing.JLabel;public class Conta implements Runnable { private JLabel etichetta; public Conta(JLabel etichetta) { this.etichetta = etichetta; } public void run() { int v = 0; while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { System.out.println(e); } v++; etichetta.setText("" + v); } }}
La prima versione dell'ascoltatore chiama direttamente run().
import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import javax.swing.JLabel;public class Ascoltatore implements ActionListener { private JLabel etichetta; public Ascoltatore(JLabel etichetta) { this.etichetta = etichetta; } public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("parti")) { Conta conta = new Conta(etichetta); conta.run(); } }}
Premendo «parti» la finestra si blocca: l'etichetta non cambia, la finestra non si ridisegna, non risponde ai pulsanti e non si chiude.
Il ciclo degli eventi
Finché non si avviano altri thread, un programma con interfaccia grafica ha un solo flusso: il thread dell'interfaccia, che esegue il ciclo principale degli eventi.
Attende il prossimo evento della finestra: un clic, un tasto, un ridimensionamento. Lo manda il sistema grafico.
Se all'evento sono associati ascoltatori, li richiama.
Ridisegna la finestra e torna al punto 1.
Quindi actionPerformed viene eseguito dal thread dell'interfaccia. Se non termina, il thread non torna al ciclo: non ridisegna, non gestisce «ferma» e non chiude la finestra. L'ascoltatore non gira in parallelo al ciclo: lo richiama il ciclo stesso.
Gli eventi che arrivano nel frattempo non vanno persi: restano nella coda degli eventi e sono gestiti quando l'ascoltatore finisce. Con un ciclo finito di 50 passi da 100 millisecondi, cioè 5 secondi, la finestra resta ferma 5 secondi. Poi l'etichetta passa di colpo a 50 e il clic su «ferma» viene gestito.
Un clic su «ferma» durante il conteggio (tempo verso destra)
clic su fermaclic gestito
ascoltatore in esecuzione (5 s)
clic in coda
Il clic su «ferma» resta in coda finché l'ascoltatore del conteggio non termina: viene gestito solo dopo 5 secondi.
Il conteggio in un thread a parte
Il thread dell'interfaccia deve restare libero. Il conteggio va quindi in un thread separato, e l'ascoltatore lo avvia e ritorna subito. Basta sostituire conta.run() con la creazione del thread, perché start() esegue run() in un flusso a parte, mentre run() lo eseguiva nel flusso chiamante.
public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("parti")) { Conta conta = new Conta(etichetta); Thread t = new Thread(conta); t.start(); }}
Ora la finestra si ridimensiona, si chiude e riceve gli eventi di «ferma» mentre l'etichetta conta. Ogni pressione di «parti» avvia un altro thread, quindi i conteggi sono più di uno e si alternano. «Ferma» non interrompe niente: il lavoro avviato non ha ancora un meccanismo di arresto.
Due pressioni di «parti»
Ogni pressione di «parti» crea un thread con il proprio Conta. L'etichetta è una sola: i due conteggi la modificano a turno.
Un'operazione che può richiedere più di un decimo di secondo non va eseguita nel thread dell'interfaccia, altrimenti la finestra resta bloccata: è una raccomandazione. Su Android l'apertura di un socket nel thread dell'interfaccia può lanciare un'eccezione e va fatta in un thread a parte.