Thread Concetti essenziali 1
Programma È l’unità di esecuzione all’interno del S.O. Solitamente, esecuzione sequenziale (istruzioni vengono eseguite in sequenza, secondo l’ordine specificato nel testo del programma). Un processo è un programma in eseguzione. S.O. multprogramma permettono l’eseguzione concorrente di più processi. 2
Thread Un Thread è un singolo flusso di esecuzione di un processo. Un processo può avere più Thread, che condividono alcune risorse. Ad ogni thread è associato in modo esclusivo il suo stato della computazione, fatto dal valore del program counter e degli altri registri della CPU e da uno stack. I thread di uno stesso processo vedono le stesse variabili: se uno dei thread modifica una variabile, la modifica è vista anche dagli altri thread. 3
Stati di un Thread Init: acquisione delle risorse Ready: thread pronto ad essere eseguito. Running: thread in eseguzione. Waiting: thread sospeso in attesa di un evento esterno. Terminated: thread concluso. 4
Multithreading Ed i relativi problemi 5
Programma multithreading – 1 È un programma formato da più thread che collaborano per poter ottenere uno scopo comune. Lo scopo comune dei thread per esempio è cambiare le ruote all’auto. Infatti è inimmaginabile che in una gara di formula 1 sia un singolo meccanico (thread) a cambiare ruote. 6
Programma multithreading – 2 Possiamo vedere un programma multithreading come un «flusso» di esecuzione indipendente dagli altri, ma connesso agli altri in un unico programma. 7
Programma multithreading – 3 L’utilizzo dei thread serve solo e solamente per bilanciare il programma. Bisogna evitare di far lavorare troppo o troppo poco un thread. Il bilanciamento del carico di lavoro è neccessario per ottimizzare il programma. 8
Programma multithreading – 4 Le risorse condivise sono quelle a cui tutti i thread possono accedere in parallelo. Se si legge e/o modifica la stessa risorsa condivisa nello stesso istante potremmo realizzare una azione che provoca dei risultati non deterministici (inaspettati). Si devono maneggiare con cautela. 9
Programma multithreading – 5 Se due processi (o thread) che operano in parallelo sono completamente indipendenti tra loro non si presenta nessun particolare problema. Ma in molti casi i processi concorrenti possono interagire tra loro con due modalità: COMPETIZIONE E COOPERAZIONE. COMPETIZIONE: I processi competono tra loro per l’accesso e l’utilizzo di una risorsa che non possono utilizzare entrambi contemporaneamente. COOPERAZIONE: I processi devono cooperare per realizzare un determinato compito. Può essere necessaria la condivisione di risorse e la comunicazione. 10
Risorse condivise – 1 L’utilizzo delle risorse condivise da parte dei thread. Quando due o più thread vengono eseguiti in maniera concorrenziale, è in generale impossibile prevedere l'ordine in cui le loro istruzioni verranno eseguite. I risultati dipendono esclusivamente dall’ordine di esecuzione delle istruzioni. I risultati non deterministici sono da evitare in tutti i modi, in quanto il programmatore NON SA cosa succede in tutti i singoli casi. Oltre a provocare risultati non deterministici possono portare ad altri errori e anche gravi. 11
Risorse condivise – 2 Bisogna limitare l’uso delle risorse condivise ad un singolo thread per volte. Se una risorsa condivisa può provare danni se usata da due thread insieme bisogna realizzare un sistema per evitare l’accesso parallelo e serializzarlo, essa è una sezione critica. La sezione critica è un processo di modifica di un insieme di variabili condivise. 12
Risorse condivise – 3 13
Risorse condivise – 4 Letture e scritture nello stesso «istante» provocano a risultati non determinabili a priori. Questo ci impone di dover realizzare delle politiche di prevenzione dei risultati non deterministici. Queste politiche rendono il la risorsa «safe». Se c’è uno context switch tra due istruzioni critiche la risorsa condivisa potrebbe essere in uno stato di inconsistenza. 14
Operazioni atomiche Un istruzione atomica è una istruzione indivisibile a livello di eseguzione: non ci può essere un context swich all’interno di una istruzione atomica. Ovvero se nessun’altra istruzione può cominciare prima che sia finita (ovvero non può esserci interleaving). In caso di modifica di una area critica in operazioni separate da context switch si rischia una inconsistenza della variabile. 15
Serializzare il parallelo – 1 Le politiche per rendere una risorsa sicura si basano sul fatto di serializzare l’accesso alla risorsa. Serializzando l’accesso si creeranno dei tempi morti. In questi tempi morti è bene mettere il processo in stato di attesa e non sprecarlo con busy wait. 16
Serializzare il parallelo – 2 Per serializzare l’accesso ad una risorsa si può usare un sistema simile a quello che avviene nei bagni: realizzare una serratura che ti dice se puoi entrare o meno all’interno della risorsa. Se non entri rimani in attesa in una coda. Questo tipo di sicurezza viene chiamato mutua esclusione (mutex). 17
Mutua esclusione La regola di mutua esclusione stabilisce che in ogni istante una risorsa o è libera oppure è assegnata a uno e un solo processo: in particolare per avere la mutua esclusione devono essere soddisfatte le seguenti quattro condizioni: nessuna coppia di processi può trovarsi simultaneamente nella sezione critica; l’accesso alla regione critica non deve essere regolato da alcuna assunzione temporale o dal numero di CPU; nessun processo che sta eseguendo codice al di fuori della regione critica può bloccare un processo interessato a entrarvi; nessun processo deve attendere indefinitamente per poter accedere alla regione critica. In una sezione critica con una mutua esclusione essa può eseguita una sola volta a ogni istante. 18
Serializzare il parallelo – 3 Ma se esistono due bagni uno accanto all’altro non ha senso realizzare due code distinte, ma bensì una univoca. Quest’altra tecnica invece è un controllo del flusso basata su semafori contatori. Come si può intuire la mutua esclusione è un semaforo che fa passare una persona alla volta. 19
Deadlock – 1 Supponiamo che il Thread 1 stia bloccando l’oggetto 1 e che voglia l’oggetto 2, bloccato dal Thread 2. Supponiamo inoltre che il Thread 2 stia bloccando l’oggetto 2 e che voglia l’oggetto 1, bloccato dal Thread 1. Questa specifica situazione di blocco a vicenda viene chiamato deadlock. 20
Deadlock – 2 21
Starvation Accade quando un Thread di bassa priorità non viene mai mandato in esecuzione in quanto ci sono altri thread con una maggiore priorità. Come quando siete in coda a uno sportello e continuano ad arrivare “furbi” che passano davanti, impedendovi di avanzare verso l’impiegato. 22
Thread safety Un programma concorrente deve godere di alcune proprietà che devono essere valide per ogni possibile “caso di esecuzione” del programma stesso: Sicurezza delle risorse condivise: i thread non devono interferire tra di loro su una modifica ad una risorsa condivisa, ci devono essere meccanismi di sincronizzazione. Liveness: bisogna garantire che il processo avanzi sempre, e quindi evitando sitauzioni di deadlock. Fiarness: tutte le richieste devono essere soddisfatte, evitando code a priorità, e quindi evitando la starvation. 23
Come realizziamo il Thread safety? Dobbiamo serializzare le richieste dell’uso di una risorsa condivisa e l’utilizzo della stessa deve avvenire per un tempo finito e breve. Così evitiamo tutti i possibili problemi derivati. Per questo sono state ideate delle primitive: esse sono delle linee guida che poi vengono implementate dai vari linguaggi di programmazione che dicono come gestire situazione. E.W. Dijkstranel 1968 ha proposto due primitive che permettono la soluzione di qualsiasi problema di interazione fra processi, che sono: la primitiva P(S), che riceve in ingresso un numero intero S non negativo (semaforo), che viene utilizzata per accedere alla risorsa. la primitiva V(S), che riceve anch’essa in ingresso un numero intero S non negativo (semaforo), che viene utilizzata per rilasciare la risorsa. 24
Semaforo di Dijkstra – 1 Un semaforo è formato da una variabile intera contatore. Se il semaforo vale zero non si può accedere alla risorsa e si deve attendere Se il semaforo vare più di zero indica libero ed il valore dice quanti processi possono ancora accedere alla risorsa. Se si usa un mutex, cioè un semaforo binario il valore massimo di thread che accedono alla risorsa è 1 25
Semaforo di Dijkstra – 2 1.Il macchinista di A controlla lo stato del semaforo (esegue una P(S)): lo trova spento, quindi lo accende (S = 0) e inizia a transitare sul ponte; 2.anche il treno B sopraggiunge al ponte ma trovando il semaforo acceso esegue anch’esso una P(S), si ferma e rimane in attesa. 3.A continua la sua corsa attraversando il ponte; 4.una volta raggiunta l’altra sponda, spegne il semaforo (S=1); 5.B ora vede il semaforo spento e lo accende (S=0) e inizia pure lui ad attraversare il ponte. 26
Primitiva P(S) Serve per acquisire la risorsa, fa un controllo ciclico se la risorsa è disponibile e quando è disponibile decrementa il valore del semaforo. Nomi comuni della implementazione della suddetta primitiva: acquire, lock, enter, wait… 27
Primitiva V(S) Incrementa il valore del semaforo ed esce dall’area critica. Essendo la variabile del semaforo un intero rende che le primitive P(S) e V(S) permettono di regolare l’acceso non solo a due processi ma anche l’accesso multiplo di numerosi processi. Nomi comuni della implementazione della suddetta primitiva: signal, release, unlock, exit… P(S) e V(S) sono primitive indivisbili 28
Utilizzo delle primitive P(S) e V(S) Prima di accedere alla zona critica del programma viene lanciata la primitiva P(S) che blocca la risorsa. Viene eseguito il codice a rischio. Viene eseguita la primitiva V(S) che rilascia la risorsa. Attenzione: è sempre necessario usare adeguati sistemi Try-Catch- Finally gestire le eccezioni e lanciare sempre ed in qualunque caso anche la V(S) dopo aver eseguito la P(S). 29
Semafori a conteggio e buffer Una delle limitazioni dei vettori in molti linguaggi di programmazione è la dimensione fissa degli array. Per evitare questo possiamo usare dei semafori per «ritardare» l’aggiunta di un elemento in attesa che il buffer si svuoti. Si realizza con il semaforo di Dijkstra a conteggio (quello non binario) 30
Sincronizzazione tra Thread Problemi 31
Problema del produttore e consumatore – 1 È un esempio classico di sincronizzazione tra processi. Il problema descrive due processi, uno produttore (producer) ed uno consumatore (consumer), che condividono un buffer comune, di dimensione fissata. Compito del produttore è generare dati e depositarli nel buffer in continuo. Contemporaneamente, il consumatore utilizzerà i dati prodotti, rimuovendoli di volta in volta dal buffer. Il problema è assicurare che il produttore non elabori nuovi dati se il buffer è pieno, e che il consumatore non cerchi dati se il buffer è vuoto. 32
Problema del produttore e consumatore – 2 Suddividiamo il problema in 3 casi: Possiamo prima analizzare il caso particolare nel caso in cui ci sia un unico produttore ed un unico consumatore ed il buffer ha una dimensione di un elemento. Poi possiamo analizzare il caso in cui ci sia un unico produttore ed un unico consumatore ed il buffer ha una dimensione fissa diversa da 1. Poi possiamo analizzare il caso generale. 33 Il problema evidenzia due problemi di sincronizzazione: Evitare che un produttore inserisca nel magazzino se esso è pieno ed evitare che il consumatore prelevi dal magazzino se esso è vuoto. Evitare che più produttori inseriscano nel magazzino in contemporanea ed evitare che più consumatori prelevino lo spesso pezzo in contemporanea.
Problema del produttore e consumatore – 3 Per gestire la sincronizzazione tra processi ci servono due semafori binari per la mutua esclusione per segnalare se il buffer è pieno o vuoto. I due riquadri sono i posti dove vengono eseguite le primitive V(S) e P(S) sui semafori binari. 34
Problema del produttore e consumatore – 4 Per gestire la sincronizzazione tra processi ci servono due semafori per segnalare se il buffer è pieno o vuoto. Il valore massimo del semaforo è la dimensione del deposito (array), mentre la minima è 0. Si nota che i possibili valori siano la dimensione del deposito + 1. I due riquadri sono i posti dove vengono eseguite le primitive V(S) e P(S) sui semafori. 35
Problema del produttore e consumatore – 5 Naturalmente per gestire più thread dobbiamo mettere dei semafori per il controllo delle aree critiche: l’inserimento e la rimozione di elementi. Quindi in questo caso abbiamo bisogno di ben 2 semafori e 2 semafori binari per gestire in sicurezza e thread safe il tutto. 36
Problema del produttore e consumatore – 6 I due riquadri blu sono i posti dove vengono eseguite le primitive V(S) e P(S) sui semafori per gestire la dimensione del magazzino. I due riquadri verdi servono per gestire in mutua esclusione le aree critiche del multithreading. 37