• Non ci sono risultati.

Programmazione concorrente in Java. Dr. Paolo Casoto, Ph.D

N/A
N/A
Protected

Academic year: 2022

Condividi "Programmazione concorrente in Java. Dr. Paolo Casoto, Ph.D"

Copied!
37
0
0

Testo completo

(1)

+

Programmazione

concorrente in Java

(2)

+ Introduzione al multithreading

La scomposizione in oggetti consente di separare un programma in sottosezioni indipendenti.

Oggetto = metodi + attributi finalizzati alla rappresentazione di una specifica entità

Spesso non è sufficiente, è necessario ripartire il programma in sotto-attività da eseguire in parallelo.

Ciascuna sotto-attività può essere definita come thread

Componente software in esecuzione come “filo” indipendente all’interno della JVM

Non associati necessariamente a thread reali della macchina fisica.

2

(3)

+ Processi e Thread

Un processo è un programma software in esecuzione con accesso ad uno specifico spazio di indirizzamento in

memoria.

Un S.O. è in grado di gestire più processi allocando in maniera variabile il tempo di CPU a ciascun processo.

Un thread è un flusso di esecuzione all’interno di un processo.

Ciascun processo può avere a sua disposizione più thread in esecuzione parallela.

Perché i thread? Eseguire task ed utilizzare risorse senza bloccare l’esecuzione del programma principale.

E.g.: interfaccia grafica reattiva.

3

(4)

+ Thread in Java

In Java ciascun thread ha il proprio stack di esecuzione ed il proprio program counter

Condividono gli uni con gli altri il medesimo spazio di memoria per l’accesso agli oggetti.

Quando programmiamo utilizzando più thread in esecuzione contemporaneamente parliamo di programmazione

concorrente.

La JVM utilizza alcuni thread per le proprie attività “sotto il cofano”

Garbage Collector

Event Dispatcher

4

(5)

+ Thread in Java

5

Da Deitel & Deitel – “Java – How To Program”

Stati di un thread in Java

(6)

+ Thread in Java: gli stati

New: il thread è stato creato ma non è ancora in esecuzione;

Runnable: il thread è in esecuzione;

Waiting: l’esecuzione del thread A è stata sospesa in attesa dell’esecuzione di un altro thread B.

A può essere nuovamente in esecuzione solo al termine di B

Timed Waiting: similare allo stato waiting, ma non con la possibilità per il thread di rientrare in esecuzione al termine dell’intervallo del timer.

E’ possibile ad esempio forzare tutti i thread che per loro natura operano in polling sullo stato di una risorsa.

6

(7)

+ Thread in Java: gli stati

Blocked: il thread è bloccato ed escluso dall’esecuzione in attesa della disponibilità di una risorsa.

e.g.: IO da filesystem;

Terminated: il thread è escluso dall’esecuzione e non può rientrarvi in nessun modo.

Conclusione naturale del flusso di esecuzione

Conclusione forzata

Ciascun thread in Java ha un priorità di esecuzione, che ne regola il periodo di accesso alla CPU

Ereditato dal thread padre

Non è una garanzia dell’ordine di esecuzione dei thread stessi.

7

(8)

+ Thread in Java

In Java è possibile definire una attività ad esecuzione parallela mediante l’implementazione dell’interfaccia

java.lang.Runnable.

Definisce un unico metodo, run, che definisce la semantica del task da eseguire.

Per eseguire un oggetto Runnable è necessario disporre di un oggetto Executor.

Crea e gestisce un gruppo di thread.

Per ciascun oggetto Runnable il metodo run è invocato in un nuovo thread.

Il metodo execute(Runnable) aggiunge un oggetto Runnable al gruppo di thread e, ove vi siano thread disponibili, ne avvia

l’esecuzione invocando il metodo run.

8

(9)

+ Executor

E’ sempre preferibile utilizzare un oggetto della classe Executor per l’avvio di oggetti Runnable ad esecuzione concorrente.

La gestione di un pool di thread consente di ottimizzare l’esecuzione grazie al riuso di thread esistenti.

Riduzione significativa dell’overhead legato alla creazione di un nuovo thread.

E’ tuttavia possibile (in particolare nelle precedenti versioni di Java) utilizzare metodologie alternative per l’avvio dei Thread.

Ad oggi sono del tutto sconsigliabili e deprecate.

L’interfaccia ExecutorService estende Executor ed

aggiunge alcune particolari funzionalità per la gestione del ciclo di vita dell’esecutore.

9

(10)

+ Esempio

10

Metodo run

Pone il thread in stato timed waiting

(11)

+ Esempio

11

Creazione con metodo statico dell’esecutore

Blocca l’accettazione di nuovi task pur continuando l’esecuzione dei task esistenti fino al completamento

Il metodo main è eseguito nel MAIN THREAD della JVM !!!

(12)

+ Sincronizzazione

E’ spesso necessario sincronizzare più thread rispetto ad una risorsa.

E.g.: più thread che aggiornano la medesima risorsa condivisa.

Solo uno dei thread deve poter accedere, in modo esclusivo, ai metodi per l’aggiornamento della risorsa.

Gli altri thread passano in stato waiting fino al rilascio della risorsa da parte del thread in esecuzione.

Uno dei thread in attesa (la selezione del thread non è prevedibile) è avviato.

Mutua esclusione dei thread in modalità di esecuzione.

Vediamo come realizzare uno dei meccanismi di sincronizzazione più utilizzati nella programmazione concorrente: i monitor.

12

(13)

+ I monitor

Tutti gli oggetti in Java sono caratterizzati dalla presenza di un monitor interno e di un instrinsic lock (o monitor lock).

Un solo thread alla volta può acquisire il monitor lock di un oggetto.

Quando un thread deve eseguire una operazione potenzialmente critica per la concorrenza deve acquisire un monitor lock.

Nel caso in cui il monitor lock sia già stato acquisito da un altro thread, il thread chiamante passa in modalità blocked.

La definizione di parti di codice soggette al monitoraggio da parte del monitor si realizza mediante l’utilizzo dell aparola riservata synchronized.

13

(14)

+ I monitor

synchronized(oggetto){

codice da eseguire }

Al termine della esecuzione il thread rilascia il monitor lock ed uno dei thread in stato blocked rispetto al monitor lock può essere eseguito

Anche i metodi di istanza possono essere caratterizzati dalla presenza del modificatore synchronized

L’esecuzione dell’intero metodo è soggetta all’acquisizione del monitor lock.

14

Oggetto rispetto al quale il thread deve acquisire il monitor lock

Un solo thread alla volta eseguirà questa porzione di codice

(15)

+ Sincronizzazione

Definire le aree del codice il cui accesso debba essere soggetto all’attività di sincronizzazione

Definire le operazioni critiche, soprattutto su dati condivisi, come synchronized, cercando di renderle il più possibile atomiche.

Riuscire a definire operazioni atomiche la cui esecuzione non può essere svolta contemporaneamente da più thread.

E’ preferibile mantenere il più ridotte possibili le aree di codice soggette al modificatore synchronized per

mantenerne elevata l’efficienza.

Soprattutto su potenziali chiamate bloccanti, quali accesso a DB o IO, per le quali il thread può mantenere il monitor lock per lunghi intervalli di tempo.

15

(16)

+ Struttura dati condivisa non sincronizzata

16

(17)

+ Produttore

Aggiunge un valore casuale ad una struttura dati condivisa e me tiene copia localmente.

Al fine di garantire la sincronizzazione utilizzeremo una struttura dati di tipo Queue ad accesso sincronizzato, la ArrayBlockingQueue

Metodi put e take per la scrittura e la lettura dalla coda.

17

(18)

+ Produttore

18

Sospendo il thread per un periodo di tempo casuale inferiore ai 3

(19)

+ Consumatore

Legge un valore da una struttura dati condivisa.

Mantiene copia del valore localmente

19

Sospendo il thread per un periodo di tempo casuale inferiore ai 3 secondi

(20)

+ Esecuzione

Nel nostro sistema manca del tutto la sincronizzazione !

Infatti eseguendolo scopriamo che…

20

Il thread produttore ha generato un totale di elementi la cui somma è 29…

Ma il thread consumatore ne ha letti solo 27

Questi valori cambiano AD OGNI ESECUZIONE

(21)

+ Introduciamo una struttura dati sincronizzata

Rivediamo l’esempio precedente modificando l’implementazione interna della classe Valore.

In particolare adottiamo come strumento per la memorizzazione del dato condiviso un oggetto della classe

ArrayBlockingQueue

Coda (tipata) di oggetti di dimensione prefissata ad accesso sincronizzato.

Quando un thread cerca di scrivere su una coda piena, rimane bloccato in attesa della lettura di almeno un valore da parte del thread consumatore

Viceversa per il thread consumatore, che legge i valori dalla coda.

La lettura dei valori “consuma” il valore presente all’interno della coda.

21

(22)

+ Struttura dati condivisa sincronizzata

22

Chiamata sincronizzata in lettura

Chiamata sincronizzata in scrittura

Lasciamo inalterato il resto del programma

(23)

+ Esecuzione

I due thread risultano sincronizzati

23

Poiché la coda ha lunghezza 1 le due operazioni, scrittura e lettura, sono

necessariamente alternate fra loro Tuttavia è vero che

lettura e scrittura sulla coda non sono

atomiche rispetto alle attività di lettura e scrittura dei due thread

Intervallo fra esecuzione di take e di println

(24)

+ Ma come funziona

ArrayBlockingQueue ?

Impostare i metodi set e get come metodi sincronizzati, il cui accesso sia a tutti gli effetti atomico.

Utilizzare un monitor lock per definire quale thread possa effettuare le attività (in lettura o scrittura) sui dati condivisi.

Se un oggetto non può eseguire una attività su un oggetto rispetto al quale si sincronizza (e.g.: la coda è piena ed il

thread di scrittura non può pubblicare) dopo aver acquisito il monitor lock passa in waiting e rilascia il monitor lock.

Quando un thread in esecuzione completa una attività che può modificare una condizione bloccante per un altro thread in waiting, lo notifica.

24

(25)

+ Metodi wait e notify

Metodi della classe Object per la gestione della sincronizzazione:

wait: notifica al thread che sta eseguendo il codice che deve spostarsi in stato waiting.

notify: notifica ad uno dei thread in attesa che è possibile passare allo stato runnable.

notifyAll: notifica a tutti i thread in in attesa che è possibile passare allo stato runnable.

In questo caso uno dei thread è posto in esecuzione, con un processo di selezione casuale.

E’ possibile invocare uno dei tre metodi solo se il thread

chiamante ha acquisito il monitor lock sull’oggetto rispetto alla quale si sincronizza.

25

(26)

+ Modifichiamo la classe Valore

26

Blocca il thread rispetto all’oggetto

Attiva tutti gli altri thread Metodi

sincronizzati

(27)

+ Esecuzione

27

I due thread risultano sincronizzati

Siamo riusciti a simulare il

comportamento della struttura dati ad

accesso concorrente.

Importantissimo:

acquisite il monitor lock sull’oggetto prima di invocare wait…

(28)

+ E se tolgo i metodi synchronized ?

28

E siamo pure fortunati … avremo potuto avere una eccezione !!!

(29)

+ Ma con più di due thread ?

Il nostro programmino non funzionerebbe a dovere.

Perché?

Semplice, quando esco dal wait non verifico nuovamente la condizione di waiting

Ma se nel frattempo una terza entità l’avesse modificata di nuovo ?

Modifichiamo la classe Valore affinché il metodo wait sia contenuto all’interno di un ciclo di verifica della condizione.

Come ?

while(condizione){

wait();

}

29

(30)

+ Ulteriori considerazioni

Fin qui tutto bene se i nostri thread operano alla stessa velocità.

Ma se operassero a velocità differenti avremo una perdita di performance, poiché la nostra applicazione sarebbe vincolata alla velocità del thread più lento

Per questo motivo possiamo pensare di introdurre un buffer che consenta al thread più veloce di continuare a lavorare ad alta velocità

Teoricamente buffer di lunghezza 1 se il consumatore è molto più veloce del produttore

Buffer di lunghezza infinita nel caso contrario.

Cercare il giusto thread off fra spazio del buffer e tempo medio di waiting dei thread

30

(31)

+ Modifichiamo ancora la classe Valore

31

(32)

+ Esempio

32

Produttore più veloce del consumatore

Buffer di dimensione 10

(33)

+ Interfaccia Lock

Tutti gli oggetti in Java hanno al loro interno il riferimento al monitor lock, oggetto che implementa l’interfaccia

java.util.concurrent.locks.Lock

Il monitor lock può essere acquisito mediante accesso ad una sezione synchronized o mediante l’invocazione del metodo lock.

Il metodo unlock consente di rilasciare il monitor lock, consentendo l’acquisizione ad eventuali thread bloccati rispetto al monitor lock

La classe ReentrantLock implementa l’interfaccia Lock lasciando accedere al monitor lock il thread in attesa da più tempo.

Fairness policy

Nessun thread può rimanere bloccato all’infinito (starvation)

33

(34)

+ Condition

Se un thread in possesso di un monitor lock determina che non può eseguire il proprio compito, si dice che sta

attendendo una condizione su un oggetto.

L’oggetto Lock consente di definire le condizioni di attesa di un thread

Insieme delle condizioni che devono essere verificate per consentire al thread l’esecuzione delle proprie attività.

Acquisito il Lock, un thread può creare una nuova condizione sul lock grazie al metodo newCondition

Restituisce un oggetto che implementa l’interfaccia java.util.concurrent.locks.Condition

34

(35)

+ Condition

Per attendere rispetto ad un oggetto Condition, il thread invoca il metodo await dell’oggetto stesso.

Quando un thread ha completato la sua attività e ritiene di poter risvegliare un insieme di thread associati ad una condition, può invocare il metodo signalAll della condition stessa.

Posso teoricamente avere più condizioni sullo stesso monitor lock !!!

Molto più potenti rispetto al synchronize

Interruzione e timeout di attesa dei thread;

Condizioni multiple di attesa per un thread;

Nessuna necessità di acquisire e rilasciare il lock nel medesimo blocco di codice.

35

(36)

+ Modifichiamo ancora la classe Valore

36

(37)

+ Esempio

37

Riferimenti

Documenti correlati

•  Per attivare il thread deve essere chiamato il metodo start() che invoca il metodo run() (il metodo run() non può essere. chiamato direttamente, ma solo

implementazione del concetto di monitor: un oggetto java è associato ad un mutex ricorsivo tramite il quale è possibile disciplinare gli accessi da parte di diversi thread allo

Mentre diversi thread possono acquisire concorrentemente il lock di sola lettura, solo un thread alla volta può acquisire il lock in scrittura. Eventuali altri thread richiedenti

•  5 filosofi sono seduti attorno a un tavolo circolare; ogni filosofo ha un piatto di spaghetti tanto scivolosi che.. necessitano di 2 forchette per poter essere mangiati; sul

 La funzione DEVE ESSERE CHIAMATA su una variabile mutex che sia già bloccata e di proprietà del thread chiamante. In tal caso la funzione pthread_mutex_unlock sblocca la

▪ Assegnare memoria e risorse per la creazione di nuovi processi è oneroso; poiché i thread condividono le risorse del processo cui appartengono, è molto più con- veniente creare

• L'uso del lock garantisce che se un thread T esegue un metodo di istanza di un oggetto, nessun altro thread che richiede il lock può eseguire un metodo sullo

• Il metodo run della classe di libreria Thread definisce l’insieme di statement Java che ogni thread (oggetto della classe) eseguirà.. concorrentemente con gli