Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Stefano Russo » 7.Programmazione multi-thread


Sommario

  • Concetto di processo e di thread
  • Creazione di un thread
  • Lanciare e sospendere un thread
  • Stati di un thread
  • Parola chiave volative

Processi e Threads

Un processo è un programma in esecuzione all’interno di un proprio spazio di indirizzamento.

Il processo è un programma eseguibile caricato in memoria e contenente

  • il valore del program counter (PC);
  • contenuto registri CPU;
  • una propria pila (stack);
  • memoria dinamicamente allocata (heap).

Processi e Threads

Un thread è un flusso di controllo sequenziale

  • ha il suo insieme di registri;
  • ha il suo stack;
  • non ha una propria area heap e/o area dati statici (a differenza dei processi).

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.

Processi e Threads

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.

  • Un thread è contenuto all’interno di un processo;
  • diversi thread contenuti nello stesso processo condividono lo stesso spazio di indirizzamento;
  • processi differenti non condividono le proprie risorse.

Context switch

Threads diversi all’interno della stessa applicazione (programma) condividono la maggior parte dello stato:

  • sono condivisi l’ambiente delle classi e la heap;
  • ogni thread ha un proprio stack delle attivazioni.

Per quanto riguarda le variabili:

  • sono condivise le variabili statiche (classi) e le variabili di istanza (heap);
  • non sono condivise le variabili locali dei metodi (stack).

Context switch

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.

Processi e Threads

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).

Programmi multithreading

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:

  • minor tempo di context-switch;
  • maggiori efficienza del caching;
  • minor tempo per creare e terminare un thread;
  • miglior e efficienza nel l a comunicazione e nella sincronizzazione (la comunicazione interprocesso richiede l ‘ intervento del kernel, per motivi di protezione, col conseguente impatto sulle prestazioni).

Es. Elaborazione concorrente

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).

Grafo di precedenza a ordinamento parziale

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)

Race Condition

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.

Concorrenza e Parallelismo

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.

Programmazione concorrente

Le due problematiche fondamentali nello sviluppo di programmi concorrenti sono:

1) Safety – assicurare la consistenza:

  • Mutua esclusione – accesso disciplinato alle risorse condivise;
  • Atomicità;
  • Condition synchronization – alcune operazioni potrebbero richiedere di essere differite se le risorse condivise non sono in uno stato “appropriato” (es. lettura da un buffer vuoto).

2) Liveness – assicurare il progresso:

  • No Deadlock – Alcuni processi possono sempre accedere alle risorse condivise;
  • No Starvation – tutti i processi possono accedere alle risorse condivise prima o poi.

Multithreading in Java

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.

Multithreading in Java

Java consente di realizzare programmi multithread in maniera standardizzata e indipendente dalla specifica piattaforma.
Java offre:

  • Primitive per definire attività indipendenti (processi, threads);
  • Primitive per la comunicazione e sincronizzazione tra attività eseguite in modo concorrente.

Inoltre, presenta delle soluzioni per la Safety e la Liveness.

Creare un Thread

Java offre due possibili tecniche per poter creare un thread (java.lang):

  • Derivazione dalla classe Thread;
  • Implementare l’interfaccia Runnable.

Creare un Thread

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().

Creare un Thread

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();

Creare un Thread

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.

Creare un Thread

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().

Stati di un Thread


Flusso di un Thread


Lanciare un Thread

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.

Lanciare un Thread

Mostra codice
 Il costruttore di Thread accetta come primo parametro un oggetto di ThreadGroup, a cui il thread appena istanziato verrà associato. A seguito della sua creazione, un thread non ha la possibilità di modificare la sua affiliazione a uno specifico ThreadGroup.

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();

Lanciare un Thread

Mostra codice

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();

Scheduling dei thread

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

  • necessario algoritmo di scheduling per ripartire equamente le risorse di calcolo.

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à.

Scheduling dei thread

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:

  • un thread a più alta priorità deve essere eseguito (preemptive);
  • il thread invoca yield() o il metodo run() finisce;
  • in un sistema “time-slicing” il suo periodo di CPU è terminato;
  • volontariamente cede il processore;
  • sleep() – wait() – condizione di I/O.

Scheduling dei thread

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).

Scheduling dei thread

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.

Scheduling dei thread

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.

Scheduling dei thread


Scheduling dei thread

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?

Scheduling dei thread

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à.

Scheduling dei thread

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.

Interruzione di un thread

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.

Interruzione di un thread

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
}

Interruzione di un thread

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();
}

Interruzione di un thread

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.

Interruzione di un thread

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().

Altri importanti funzioni sui thread

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().

Altre importanti funzioni sui thread

Mostra codice

Dove threads[i].join(); è il processo padre che attende fino alla conclusione da parte di threads[i] della sua esecuzione.

La parola chiave volatile

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.

I materiali di supporto della lezione

S. Ancha, “Working with threads” (disposibile sul sito docenti del corso)

B. Eckel, “Thinking in Java”, capitolo 13

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion