Un processo è un programma in esecuzione all’interno di un proprio spazio di indirizzamento.
Il processo è un programma eseguibile caricato in memoria e contenente
Un thread è un flusso di controllo sequenziale
Vantaggi nell’uso dei thread
La creazione/terminazione di un thread è molto più efficiente della creazione di un processo.
La comunicazione tra threads è molto più semplice ed efficiente di quelle tra tra processi poiché non coinvolge il kernel.
Il Context switching tra threads ha decisamente un minor overhead di quello tra processi.
Si può dire che un processo è detto anche “processo pesante”, in riferimento al contesto (spazio di indirizzamento, stato) che si porta dietro.
Un thread è detto anche “processo leggero”, perché ha un contesto più semplice.
Threads diversi all’interno della stessa applicazione (programma) condividono la maggior parte dello stato:
Per quanto riguarda le variabili:
Il cambio contesto tra threads di un programma Java viene effettuato dalla JVM (Java Virtual Machine.
I threads condividono una gran parte dello stato.
Il cambio di contesto fra due threads richiede tipicamente l’esecuzione di meno di 100 istruzioni.
Un programma sequenziale ha un solo flusso di controllo (progr. single-threaded).
Un programma multi-threaded si configura come un unico processo di elaborazione in grado di eseguire più task concorrentemente (ha più punti di esecuzione).
Consentono di avere più punti di esecuzione in un certo istante, uno in ognuno dei suoi thread
Sono da preferirsi ai programmi multiprocesso per i seguenti motivi:
Esempio: Supponiamo di dover scrivere le istruzioni che codificano l’algoritmo di valutazione della seguente espressione aritmetica:
(a-b) * (c+d)+(e*f)
In figura è riportato il grafo relativo all’esecuzione dell’algoritmo di valutazione dell’espressione: (a-b)*(c+d)+(e*f).
Mette in evidenza esclusivamente le precedenze tra gli eventi dell’elaborazione che è necessario rispettare
Cosa succede se ris viene calcolato prima che r1 sia disponibile?
Ci sono degli eventi che non hanno alcuna relazione temporale (calcolo di r2 r3 e r1)…il risultato è indipendente dall’ordine in cui vengono calcolati (r2 r3 e r1 possono essere eseguiti in concorrenza)
Un condizione in cui più processi leggono o scrivono dati condivisi e il risultato finale di tali operazioni dipende dall’ordine (e dalla velocità) di esecuzione delle istruzioni dei processi.
Nei sistemi multithreading concorrenti più thread concorrono nell’uso di un’unica CPU (risorsa).
In generale, i threads sono raggruppati in lotti (chunks), ognuno dei quali condivide una stessa risorsa.
Le due problematiche fondamentali nello sviluppo di programmi concorrenti sono:
1) Safety – assicurare la consistenza:
2) Liveness – assicurare il progresso:
Java supporta la programmazione multithread al livello di linguaggio.
Tipicamente, i thread sono implementati a livello di sistema, richiedendo un’interfaccia di programmazione dipendente dalla piattaforma su cui girerà il programma e distinta rispetto al linguaggio di programmazione.
Es. C++: libreria pthread, POSIX Threads Programming, per implementare un programma multithread su piattaforme UNIX.
Java consente di realizzare programmi multithread in maniera standardizzata e indipendente dalla specifica piattaforma.
Java offre:
Inoltre, presenta delle soluzioni per la Safety e la Liveness.
Java offre due possibili tecniche per poter creare un thread (java.lang):
Perché Java offre due diverse soluzioni per la creazione di un thread?
Java non consente la derivazione multipla.
Se la classe utente non è già coinvolta in un legame di derivazione, possiamo usare la soluzione di derivare dalla classe Thread.
Se la classe utente è già coinvolta in un legame di derivazione, possiamo usare la soluzione di implementare l’interfaccia Runnable, ridefinendo il metodo run().
La classe utente deriva da Thread, ridefininendo il metodo run().
public class MyClass
extends Thread{
public void run(){ doWork();}
}
...
Thread t = new MyClass();
t.start();
La classe utente implementa l’interfaccia runnable ridefininendo il metodo run().
public class MyClass
extends ParentClass
implements Runnable{
public void run(){ doWork();}
}
...
Runnable r = new MyClass();
Thread t = new Thread(r);
t.start();
Un’istanza della classe che implementa l’interfaccia Runnable viene fornita al costruttore di Thread per istanziarne un oggetto.
Il metodo start()
ha come obiettivo quello di allocare un thread nella JVM, determinando l’invocazione di run()
Il metodo run()
definisce il comportamento del thread
Il metodo sleep(long millisec)
sospende l’esecuzione del thread per il periodo di tempo specificato
Il metodo yield()
causa la sospensione del thread a favore di un altro thread
NOTA
I thread in Java non hanno alcuna funzione diexit
. Il thread verrà deallocato solo alla fine del metodo run()
.
All’atto della loro creazione, i thread posso essere raggruppati per mezzo di ThreadGroup
, così da poterli controllare congiuntamente come se fossero una singola entità.
Ogni thread appartiene sempre a un gruppo; se non specificato, un thread appartiene a un gruppo di default chiamato main.
Creo il gruppo e associo ogni thread al gruppo.
ThreadGroup group = new ThreadGroup("Queen bee");
Thread t1 = new Thread(group, "Worker 1");
Thread t2 = new Thread(group, "Worker 2");
Lancio ogni singolo thread.
t1.start();
t2.start();
Posso omettere di invocare start()
dopo aver istanziato un oggetto della sottoclasse di Thread se inserisco tale chiamata nel costruttore
public class MyThread extends Thread {
public MyThread() {
super(); start();}
public void run() {...}
}
Eseguo una interrupt() su tutti i thread associati al gruppo
group.interrupt();
A causa della natura non deterministica della JVM, non si ha la certezza di quando un nuovo thread potrà essere eseguito, ma solo che lo sarà prossimamente.
Nei sistemi monoprocessore un solo thread alla volta può essere eseguito
Java supporta l’agoritmo “fixed priority scheduling“, basato sulle priorità di ogni thread.
In ogni momento, se c’è più di un Runnable thread in attesa, il sistema sceglie quello a più alta priorità.
Se ci sono più threads con la stessa priorità, il sistema ne sceglie uno operando in modalità “First Come-First Served
“.
Il thread scelto può continuare l’esecuzione fino a che:
preemptive
);time-slicing
” il suo periodo di CPU è terminato;Java prevede che i thread siano caratterizzati da una certa priorità.
Ogni thread eredita la priorità della classe che lo genera;
Il valore di default per la priorità è pari a NORM_PRIORITY
, ma è possibile cambiare la priorità di un thread mediante il metodo setPriority(int).
I threads che si trovano nello stato runnable vengono schedulati in base alla loro priorità, che può essere compresa tra i valori:
Java.lang.Thread.MIN_PRIORITY
java.lang.Thread.MAX_PRIORITY
NB: Il mapping delle priorità dipende dalla piattaforma sulla quale la VM esegue!
L’algoritmo di scheduling è di tipopreemptive
: se un Thread a priorità maggiore di quella di tutti gli altri diviene runnable, questo verrà mandato in esecuzione per primo.
Analizziamo un esempio del metodo run di un thread:
public class SelfishRunner
extends Thread {
public int tick = 1;
public void run () {
while (tick < 4000000) {
tick++;
}
}
}
Una volta in esecuzione, tale thread continua fino alla terminazione del ciclo o fino all’arrivo di un altro thread a priorità maggiore.
I thread con la stessa priorità di SelfishRunner potrebbero aspettare a lungo.
Alcuni sistemi limitano i threads egoisti (selfish) con il time slicing: l’esecuzione di più thread, eseguiti in alternanza solo per uno specifico quanto di tempo (slice).
Si applica quando più thread con identica priorità hanno diritto ad essere eseguiti e non ci sono altri threads a priorità più elevata.
NB: La piattaforma Java non implementa il time slicing! Infatti, non si può far affidamento sul time sharing dato che i risultati sarebbero differenti da architettura ad architettura.
Quale soluzione adotta Java per risolvere il problema di thread egoisti?
Il programmatore ha a sua disposizione i metodi della classe Thread per forzare i threads alla collaborazione.
ll metodo yield()
, ad esempio, permette al sistema di cedere la CPU ad un altro thread eseguibile con la stessa priorità.
Il sistema alternerà i threads nello stato Runnable con un algoritmo round-robin. Nel caso di tre threads si avrà un output del tipo:
Thread # 0, tick 500000
Thread # 1, tick 500000
Thread # 2, tick 500000
Thread # 0, tick 1000000
Thread # 1, tick 1000000
Thread # 2, tick 1000000
Thread # 0, tick 1500000
Thread # 1, tick 1500000
Thread # 2, tick 1500000
Thread # 0, tick 2000000
Thread # 1, tick 2000000
Thread # 2, tick 2000000
La presenza dell’istruzione yield() tende a rendere l’esecuzione del programma deterministica, indipenden-temente dal sistema operativo (time-slicing o meno) e dalle risorse di calcolo disponibili.
Un’interruzione indica che un thread deve interrompere quello che sta facendo e fare qualcosa altro.
È compito del programmatore indicare cosa deve fare un thread a seguito di una notifica di interruzione.
Un thread solleva un’interruzione su di un altro thread, invocando il metodo interrupt()
sull’oggetto Thread da interrompere.
Molti metodi che un thread può invocare durante la sua esecuzione, sollevano una eccezione di tipo InterruptedException
quando il metodo interrupt() viene invocato durante la loro esecuzione. In questo caso, basta gestire tale eccezione opportunamente.
try {
... metodo che può sollevare Interrupted Exception ...
} catch (InterruptedException ex) {
codice gestione eccezione
}
Se invece il thread non invoca alcun metodo che potrebbe sollevare eccezioni di tipo InterruptedException, allora deve periodicamente invocare il metodo Thread.interrupted()
, che ritorna true se sul thread è stato invocato il metodo interrupt():
for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if( Thread.interrupted() ) {
//thread interrotto...
return;
}
}
Oppure, in alcune circostanze, ha senso sollevare una eccezione se interrupted() restituisce true:
if( Thread.interrupted()) {
throw new InterruptedException();
}
Oltre a interrupt(), la classe Thread presenta tre ulteriori metodi per l’interruzione di un thread:
suspend()
– sospende un thread;resume()
– riattiva un thread precedentemente interrotto da suspend()
;stop()
– ferma un thread e lo uccide.Questi metodi sono deprecati, ovvero si consiglia ai programmatori di non adoperarli.
l’uso di tali metodi è pericoloso e comporta notevoli complicazioni: un thread può essere interrotto prima di poter rilasciare una risorsa, impedendo così agli altri di potervi accedere e generando un deadlock difficilmente risolvibile.
Per questa ragione, si assume che un thread si interrompe e muore solo a conclusione del suo metodo run().
Un metodo della classe Thread comunemente adoperato dai programmatori Java è sleep()
, che pone in attesa (o stato dormiente) un thread per il numero di millisecondi che viene specificato dall’utente.
try {
Thread.sleep(4000);
} catch (InterruptedException ex) {
System.out.println(ex);
}
Quando un thread padre genera vari thread figli, potrebbe essere necessario attendere la loro conclusione, prima di procedere. Allo scopo è possibile usare il metodo join()
.
Dove threads[i].join();
è il processo padre che attende fino alla conclusione da parte di threads[i] della sua esecuzione.
Per ragioni prestazionali, JVM consente a ogni thread di avere una copia di lavoro delle variabili membro condivise, tale copia solo occasionalmente (quando un thread entra ed esce da un blocco sincronizzato) si coordina con la variabile originaria.
La parola chiave volatile
avvisa la JVM che non deve tenere una copia privata della variabile, ma questa deve interagire direttamente con l’originaria variabile condivisa.
La parola chiave volatile
è usata come modificatore per le variabili membro, così da forzare ogni thread a rileggerne il valore dalla memoria condivisa ogni volta che la variabile è acceduta.
2. La modellazione a oggetti e il linguaggio UML (richiami)
3. Generalità su Java e la programmazione ad oggetti
6. Regole di traduzione da UML a Java/C++
7. Programmazione multi-thread
8. Sincronizzazione tra thread
9. Programmazione client-server con socket TCP/IP (Java networkin...
10. Programmazione di applicazioni client-server: il Pattern Proxy...
12. Design Patterns
13. Pattern architetturali - Esempi
14. Design pattern creazionali. Esempi
15. Design pattern strutturali. Esempi
16. Introduzione alle tecnologie middleware
17. Modelli di middleware: RPC, MOM, TP, TS
S. Ancha, “Working with threads” (disposibile sul sito docenti del corso)
B. Eckel, “Thinking in Java”, capitolo 13