Comunicazione fra thread

A cosa serve

Gli oggetti con un comportamento hanno ognuno un thread.

Per esempio, se esistono tre oggetti Autobus a1, a1, a1, due oggetti Autorimessa r1, r2 e quattro oggetti Autista u1, u2, u3, u4, allora servono tre, due e quattro thread, quindi nove in totale.

[thread-01.fig]

Ogni thread riceve i messaggi di un oggetto e si comporta di conseguenza, eventualmente inviando altri messaggi. Per esempio, un oggetto a2 può ricevere un messaggio da un autista u3, e mandare di conseguenza un messaggio all'autorimessa r2.

[thread-02.fig]

Questo richiede che un thread mandi un messaggio a un altro thread. Dato che i thread procedono tutti insieme, più messaggi potrebbero venire inviati insieme. Il ricevente li riceverà uno per volta.

[thread-03.fig]

Come inviare e ricevere messaggi

Quando il thread di a2 riceve un messaggio da u3, deve fare delle operazioni. Dal momento che in Java le operazioni si possono inserire solo in un metodo, deve essere eseguito quel metodo.

Questo metodo non può venire eseguito direttamente dal thread di u3. Anche se si tratta di un metodo della classe Autobus, verrebbe comunque eseguito dal thread di u3.

Viene lanciato un thread per ogni oggetto Autobus, ma i thread per Java non sono collegati agli oggetti Autobus. Il thread di a2 per Java è un thread come tutti gli altri. Come anche il thread di u3. La differenza è che nei loro metodi run() il primo esegue le azioni di a2, il secondo esegue le azioni di u3. È il primo che deve eseguire l'azione di risposta al messaggio, non il secondo. È il thread di a2, non quello di u3.

Il sistema per inviare è e ricevere messaggi sfrutta la condivisione della memoria:

Prima implementazione: busy wait

Per semplicità, i messaggi potrebbero essere scritti e letti in una variabile di tipo stringa, per esempio una variabile statica di una classe. In una prima implementazione Busy.java, il destinatario controlla continuamente se questa variabile contiene una stringa invece di null.

public class Busy {
	static String messaggio;

	public static void main(String[] args) throws InterruptedException {
		…
		Busy.messaggio = null;
		…
	}
}

Il thread mittente inserisce il messaggio nella variabile.

class Mittente implements Runnable {
	@Override
	public void run() {
		try { Thread.sleep(1000); } catch (InterruptedException e) {}
		Busy.messaggio = "ciao";
		System.out.print("messaggio inviato");
	}
}

Il thread destinatario controlla continuamente se ci sono messaggi. Se ne trova uno, reagisce di conseguenza e lo cancella dalla variabile statica, così ne può riceverne altri.

class Destinatario implements Runnable {
	@Override
	public void run() {
		while (Busy.messaggio == null) {
		}

		System.out.print("	messaggio ricevuto: ");
		System.out.println("\"" + Busy.messaggio + "\"");
		Busy.messaggio = null;
	}
}

Problemi del busy wait e soluzioni

La ricezione di messaggio è un evento asincrono: può avvenire in qualsiasi momento. Questo momento non dipende dal thread ricevente, che non lo può prevedere. Non dipende da esso ma dagli altri thread. Ci sono tre modi di gestire gli eventi asincroni:

busy wait
il ricevente controlla compulsivamente l'arrivo di messaggi
consuma tempo a scapito degli altri thread e programmi, consuma energia (batteria), etc.
blocco
il ricevente si blocca finchè il messaggio non arriva
lascia girare gli altri thread e programmi, non consuma energia
non può fare altre cose nel frattempo
interruzione
il ricevente fa altre cose, ma quando arriva un messaggio vengono interrotte per gestirlo

Il busy wait viene generalmente evitato. La prima soluzione è che il thread destinatorio sospenda la sua esecuzione finchè non ci sono messaggi. In Java, questo si può realizzare con la sincronizzazione e i metodi wait(); e notify().

Blocco e sblocco

Per bloccare e sbloccare un thread:

primo thread				secondo thread

arriva al blocco sincronizzato
lo trova libero: entra

					arriva al blocco sincronizzato
					lo trova occupato: si ferma

esegue delle operazioni

wait()
1. sospende l'esecuzione
2. libera temporaneamente il blocco

					ora il blocco sincronizzato è libero
					entra nel blocco sincronizzato

					esegue delle operazioni

					notify()

non riparte perchè il blocco è occupato

					esegue altre operazioni

					esce dal blocco sincronizzato

riparte

esegue altre operazioni

esce dal blocco sincronizzato

[sequenza-03.fig]

[sequenza-05.fig]

[sequenza-07.fig]

[sequenza-09.fig]

[sequenza-11.fig]

[sequenza-13.fig]

[sequenza-14.fig]

[sequenza-16.fig]

[sequenza-18.fig]

Una implementazione è in Blocco.java. Contiene solo il meccanismo di blocco e sblocco, manca la scrittura e la lettura del messaggio.

Questa viene aggiunta in Messaggio.java. Il messaggio viene scritto in una variabile statica stringa Messaggio.messaggio. Il blocco e lo sblocco avvengono sulla stringa letterale "abcd".

Coda di messaggi

Dato che i thread procedono insieme, uno stesso thread può ricevere più messaggi insieme.

[thread-03.fig]

I messaggi vanno letti uno per volta. Un modo è scriverli in fondo a una lista e leggerli dalla cima, o viceversa.

[multipli-02.fig]

Una prima implementazione Coda.java usa una lista memorizzata in una variabile statica su cui scrive un messaggio in un thread e ne legge uno in un altro.

Invece di usare "abcd" per la sincronizzazione, il blocco e lo sblocco, usa la lista stessa. Non cambia niente, dato che potrebbe essere un oggetto qualsiasi al posto di "abcd", basta che sia lo stesso sia per la sincronizzazione che per wait() e notify(). Non si può fare nell'implementazione precedente con singola variabile String perchè l'oggetto memorizzato nella variabile statica è inizialmente null e cambia ogni volta che la variabile viene cambiata. Invece l'oggetto lista rimane lo stesso, cambia solo il suo contenuto.

Una seconda implementazione Iterato.java legge e scrive più messaggi nello stesso modo.

Caso generale: più produttori e più consumatori

[thread-03.fig]

La comunicazione da più thread verso uno passa per una lista collegata.

[multipli-02.fig]

Lo stesso sistema si può usare nel caso più generale in cui sia i generatori di messaggi che i loro ricevitori sono più di uno.

[multipli-03.fig]

La differenza è che ora più thread possono essere bloccati in attesa che qualcuno metta un messaggio in lista quando questa è vuota.

Sono possibili tre casi:

il messaggio può venire gestito da un destinatario singolo qualsiasi
si continua a usare notify(), che sblocca uno qualsiasi degli oggetti bloccati
il messaggio deve essere ricevuto da tutti i destinatari
si usa notifyAll(), che sblocca tutti i destinatari bloccati
il messaggio deve essere gestito da un destinatario specifico
si usa notifyAll(), che sblocca tutti i destinatari bloccati; i destinatari leggono il messaggio e lo ignorano se non è diretto a loro

Implementazione UML

L'implementazione UML usa una lista per ognuno dei destinatari. Ricade quandi nella situazione specifica di singolo destinatario.

Per ogni oggetto con un comportamento viene creata una lista e lanciato un thread. Questo avviene per ogni oggetto Autobus, per ogni oggetto Autorimessa, e per ogni oggetto Autista. Per ognuno di loro c'è una lista e un thread: