

































































Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
appunti di programmazione concorrente
Tipologia: Appunti
1 / 73
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!


































































! Legge di Moore: il numero di transistor su un circuito tende a raddoppiare ogni due anni !Per anni questa legge è stata confermata, in quanto per svolgere in 5 secondi un task che su una CPU single Core ad 1 GHz ne impiegava 10, si passava semplicemente ad una CPU single Core a 2 GHz !Un problema chiamato “ thermal noise ” è stato in grado di rompere la Legge di Moore !esso è legato alla termodinamica e ha a che fare con la necessità di alimentazione a basso voltaggio per il raffreddamento dei transistor e l’obiettivo di aumentare la velocità del clock, che invece porta a surriscaldamenti !In pratica non si possono avere su un processore tanti transistor che siano facili da raffreddare e veloci, ma si deve rinunciare a una tra queste caratteristiche !il therrnal noise inizia ad avere effetto con la tecnologia al di sotto di 40nm (il core i7 usa 45nm), ma la Intel sta già studiando la tecnologia a 32nm !Fino a poco tempo fa, “ progresso tecnologico ” significava incremento della velocità del clock del processore, che si traduceva direttamente in un incremento della velocità di esecuzione del software !Con l’introduzione delle CPU multicore, “ progresso tecnologico ” significa incrementare il parallelismo e non la velocità del clock, ovvero più transistor ma organizzati in core multipli !sfruttare questo parallelismo è una delle maggiori sfide dell’Informatica moderna !questo perché la frequenza di clock si è assestata intorno ai 3GHz e non sembra dover cambiare nei prossimi anni !ciò significa che le applicazioni che sono lente oggi, saranno lente anche domani se non fanno uso di un ambiente di esecuzione parallelo !l’ultima generazione di chips della Intel ha già 8 core !Lo sviluppo dei microprocessori è, quindi, rivolto alla produzione di CPU multicore, dove ogni core comunica con gli altri attraverso una cache condivisa !ciò che è richiesto a programmatori e sviluppatori è cambiare paradigma, ma la storia insegna con l’avvento della programmazione Object-Oriented nei primi anni ’90, che sebbene ci voglia del tempo, il cambiamento è gestibile ! Sfide !quando si scrivono programmi per un solo processore, i dettagli architetturali della macchina vengono ignorati !sfortunatamente, quando i programmi sono destinati a macchine con multi-processori, bisogna tener presente le caratteristiche dell’architettura e come si può trarre vantaggio dall’uso dei thread, gestendo, però, i problemi di sincronizzazione !un singolo thread accede alla memoria, la quale è strutturata ad oggetti !la situazione si complica notevolmente se si usano più thread che devono accedere alle stesse aree di memoria !il trend del presente/futuro è chiaramente in direzione multi-core: siano essi omogenei (come quelli Intel e Sun), eterogenei (AMD) o una via di mezzo (IBM), saranno numerosi !secondo alcune ipotesi i core nel 2017 saranno: 128 sui desktop, 512 sui server e 4096 sui sistemi embedded ! Test-And-Set e Test-Test-And-Se t !supponiamo che due thread che condividono una risorsa, debbano usarla in mutua esclusione !ogni thread deve fare il lock della risorsa prima di usarla e fare unlock dopo averla usata !assumiamo che lo stato del lock sia una semplice variabile booleana: se è false il lock è libero, altrimenti è in uso !per manipolare il lock, il metodo getAndSet(v) fa uno swap atomico del parametro con lo stato: se l’invocazione getAndSet(true) restituisce false allora si è acquisito il lock, altrimenti il parametro era già locked e non si è acquisito il diritto di accesso alla risorsa !Un thread rilascia un lock semplicemente assegnando false al campo booleano ! Test-And-Set !il lock test-and-set (TASLock) chiama ripetutamente getAndSet( true ) per leggere il campo lock, finché non viene restituito false ! Test-Test-And-Set !il lock test-test-and-set (TTASLock) legge il campo lock attraverso state.get() finchè questo restituisce false !solo a quel punto chiama getAndSet() !NOTA: la lettura del valore lock è atomica, come anche l’applicazione di getAndSet() al valore di lock ma la loro combinazione non lo è: tra il momento in cui il thread legge il valore lock ed il momento in cui chiama getAndSet(), il valore di lock potrebbe essere modificato da altri thread public class TASLock implements Lock { … public void lock() { while (state.getAndSet(true)) {} } … } public class TTASLock implements Lock { … public void lock() { while (true) { while (state.get()) {}; if (!state.getAndSet(true)) return; } } … }
! Continuo Test-And-Set e Test-Test-And-Se t !TASLock e TTASLock sono logicamente equivalenti ed entrambi garantiscono la mutua esclusione, sebbene TASLock sembri essere più semplice !Mentre le due implementazioni sono logicamente equivalenti, le loro performances sono molto diverse !nel 1989 Anderson ha misurato il tempo necessario ad eseguire un semplice programma di testing su diversi multiprocessori ! Anderson ha misurato il tempo trascorso per n threads per eseguire una piccola sezione critica un milione di volte, ottenendo i risultati a lato !in un mondo perfetto, entrambe le curve di TASLock e TTASLock sarebbero piatte come la “ Ideal Lock ”, dato che ogni esecuzione esegue lo stesso numero di incrementi !invece, entrambe le curve sono inclinate verso l’alto, indicando che il ritardo indotto dai lock cresce con il numero di thread !TASLock, all’apparenza più semplice, risulta essere molto più lento di TTASLock, specialmente al crescere del numero di thread ! Processori e Threads !un multiprocessore consiste di diversi processori, ognuno dei quali esegue un programma sequenziale !l’unità base di tempo è il ciclo, cioè il tempo che un processo impiega per prelevare (fetch) ed eseguire un’istruzione singola ! i tempi di ciclo cambiano * all’avanzare della tecnologia !da circa 10 milioni di cicli per secondo nel 1980 siamo passati a circa 3000 milioni nel 2005
! Spinning !un processore è in spinning se testa ripetutamente una parola in memoria, in attesa che un altro processore ne cambi il valore !lo spinning può avere conseguenze catastrofiche sulle performance del sistema !su una architettura SMP senza cache, ogni volta che il processore legge la memoria, consuma banda dal bus senza realizzare alcun lavoro utile, non permettendo a processori che hanno lavoro utile da compiere di utilizzare il bus !su una architettura NUMA senza cache, lo spinning può essere accettabile se la variabile è nella memoria locale del processore !su una architettura SMP o NUMA con cache, lo spinning consuma meno risorse !alla prima lettura, si ha una cache miss e la variabile viene caricata in cache !se la variabile non è modificata, si continua a leggere dalla cache (senza usare risorse, siano esse il bus condiviso oppure la comunicazione con un altro processore) [ local spinning ] !appena la variabile risulta modificata si ha un cache miss, si carica il valore e si ferma lo spinning. CACHE-CONSCIOUS PROGRAMMINGS !spieghiamo ora perché TTASLock supera TASLock nelle performances !ad ogni getAndSet(true) di TASLock viene generato traffico fino a saturare, specialmente nelle architetture SMP, il bus condiviso, ritardando agli altri thread, inclusi quelli che tentano di rilasciare un lock o che non sono in contesa per lock !lo spinning di TTASLock legge da una copia in cache e non produce carico sul bus, ottenendo, così, performances migliori !TTAS non è, però, ideale: quando il lock viene rilasciato, tutte le copie in cache sono invalidate, tutti i thread che erano in attesa chiamano getAndSet(true), aumentando il carico del bus, meno di TASLock, ma comunque in modo significante ! False sharing !il false sharing si verifica quando processori che dovrebbero accedere ai dati logicamente distinti si trovano in una situazione di conflitto perché le locazioni sono sulla stessa cache line !è necessario trovare un compromesso, in quanto cache line ampie aumentano la possibilità di sfruttare la località, ma aumentano anche la possibilità di false sharing !per evitare false sharing si dovrebbe poter avere un controllo a grana fine sui dati, che è possibile in C/C++, ma poco in !a tal proposito, ci sono alcuni modi per strutturare i dati in modo da evitare false sharing !oggetti o campi ai quali si accede indipendentemente dovrebbero essere allineati e riempiti in modo da finire su cache line differenti !mantenere i dati read-only separati da dati modificati di frequente !quando possibile, dividere un oggetto in pezzi locali al thread ! es: un contatore usato per statistiche potrebbe essere diviso in un array di contatori, uno per thread, ognuno risiedente su una cache line differente; così ogni thread potrebbe aggiornare la sua replica personale senza causare traffico !se un lock protegge dati modificati di frequente, porre il lock e i dati su cache line differenti, in modo che i thread che tentano di acquisire il lock non interferiscano con l’accesso del detentore del lock ai dati !se un lock protegge dati non contesi di frequente, mettere il lock e i dati sulla stessa cache line, in modo che acquisendo il lock verranno caricati solo alcuni dei dati nella cache ARCHITETTURE MULTI-CORE E MULTI-THREAD !in un’architettura multi-core processori multipli sono piazzati sullo stesso chip !ogni processore tipicamente ha una sua cache L1, ma di solito tutti loro condividono una cache L2, attraverso la quale possono comunicare efficientemente, evitando di attraversare la memoria e di invocare l’ingombrante protocollo di coerenza della cache !in un’architettura multi-thread, un singolo processore può eseguire due o più thread alla volta !molti processori moderni hanno un sostanziale parallelismo interno: essi possono eseguire istruzioni senza un ordine, o in parallelo, o anche mixando istruzioni da stream multipli per mantenere le unità hardware occupate !le architetture dei processori moderni combinano multi-core al multi-threading, dove diversi core multi-thread possono risiedere sullo stesso chip !i context switch su alcuni chips multi-core sono poco costosi e sono eseguiti ad una granularità molto fine, essenzialmente ad ogni istruzione !in questo modo, il multi-threading serve a nascondere l’alta latenza dell’accesso alla memoria, dato che ogni volta che un thread accede alla memoria, il processore permette l’esecuzione ad un altro thread ! Consistenza rilassata della memoria !quando un processore scrive un valore in memoria, quest’ultimo viene immagazzinato in cache e marcato come dirty per indicare che deve essere ancora riscritto in memoria !sui processori più moderni, le richieste di scrittura non sono applicate alla memoria appena emesse, ma vengono poste in un write buffer (buffer = coda hardware) ed applicate alla memoria in un secondo momento
! Continuo Consistenza rilassata della memoria ! Vantaggi : * batching: è spesso più efficiente emettere più richieste tutte in una volta
!la programmazione su multiprocessori è interessante perché i computer moderni sono asincroni, le attività possono arrestarsi oppure ritardare senza segnalazioni tramite interrupt, preemption, cache miss, failures ed altri eventi !un passo importante nella comprensione della computabilità è la specifica e la verifica di cosa un programma effettivamente fa !La correttezza di un programma per multiprocessore, per la sua natura, è molto più complessa di quella di un programma sequenziale e richiede una serie di strumenti differenti per poter essere dimostrata !La Safety è la proprietà che assicura che alcune condizioni negative non si verifichino mai !essa è estremamente difficile da dimostrare, in quanto bisogna provare tutti i vari stati in cui un thread può trovarsi !La Liveness assicura che una particolare condizione positiva si presenterà, cioè che il programma farà progressi verso una soluzione !Supponiamo che il primo giorno di lavoro il nostro capo ci chieda di trovare tutti i primi tra 1 e 10^10 usando una macchina parallela che supporta dieci thread concorrenti !questa macchina è noleggiata al minuto, rendendo il programma più costoso all’aumentare del tempo impiegato ! Approccio possibile: diamo ad ogni thread una quantità uguale del dominio condiviso !in pratica, ad ogni thread viene assegnata una porzione di 10^9 di numeri da controllare !fallisce perché una divisione uguale del dominio non garantisce un uguale carico di lavoro !i primi non si presentano uniformemente (tra 9*10^9 e 10^10 è difficile trovare dei numeri primi) e per appurare che un numero grande sia primo, si impiega più tempo che con un numero piccolo !non ci sono, quindi, motivi per credere che in questo modo il lavoro sarebbe ripartito equamente tra i thread ed inoltre non è chiaro quale thread debba svolgere il lavoro più oneroso !i thread che lavorano di più sono più lenti e rallentano il tempo di completamento dell’intero programma
! Continuo Idea ! Teorema: gli animali di Alice e Bob non sono mai insieme nel giardino ! Dim: per contraddizione assumiamo che ci sia un conflitto, cioè che entrambi gli animali siano nel giardino ed entrambe le bandiere siano alzate !Consideriamo l’ultima volta in cui Alice e Bob hanno alzato la propria bandiera e hanno guardato l’uno la bandiera dell’altra !Quando Alice ha guardato per l’ultima volta, la sua bandiera era completamente alzata: non ha visto la bandiera di Bob (o non avrebbe rilasciato il gatto), quindi Bob non aveva ancora completato l’alza-bandiera quando Alice ha cominciato a guardare !Ne segue che quando Bob ha guardato per l’ultima volta dopo aver alzato la sua bandiera, lo ha fatto dopo che Alice ha iniziato a guardare, quindi Bob deve aver visto la bandiera di Alice, senza così rilasciare il suo cane! nessun conflitto !contraddizione! ■ ! Proprietà della mutua esclusione !questo protocollo garantisce la mutua esclusione , cioè i due animali non si troveranno mai nel giardino nello stesso momento !oltre alla mutua esclusione, un’altra proprietà importante è la deadlock freedom !garantisce che *se un animale vuole entrare nel giardino, allora ciò accade *se entrambi vogliono accedere, almeno uno dei due riuscirà a farlo !il protocollo usato da Alice e Bob è deadlock-free !supponiamo che entrambi gli animali vogliano usare il giardino !Alice e Bob alzano ognuno la propria bandiera !Bob (WLOG) si accorge che la bandiera di Alice è alzata e abbassa la sua, permettendo al gatto di Alice di entrare nel giardino !un’altra proprietà interessante è la starvation-freedom : se un animale vuole accedere al giardino, riuscirà prima o poi a farlo? !qui il protocollo di Alice e Bob funziona male: ogni qual volta Alice e Bob sono in conflitto, Bob cede il passo ad Alice !così, è probabile che il gatto di Alice usi il giardino per molto tempo ancora, a discapito del cane di Bob !un’ultima proprietà è la waiting: immaginiamo che Alice alzi la bandiera e, a causa di un attacco di appendicite, debba correre in ospedale per qualche settimana !durante questo periodo, Bob deve aspettare poiché vede alzata la bandiera e non può far uscire il suo cane !il problema è che il protocollo decide che Bob debba attendere che la bandiera di Alice sia abbassata !se Alice viene ritardata, lo stesso avviene per Bob !normalmente, ci si aspetta che Alice e Bob rispondano in un tempo limitato, ma cosa accade se non lo fanno? Il problema della mutua esclusione richiede waiting: nessun protocollo di mutua esclusione lo evita !la questione dell’attesa è importante come esempio di tolleranza ai guasti (fault-tolerance). ! La Morale !Nei sistemi concorrenti si possono verificare due tipi di comunicazione: ! comunicazione transiente : richiede che entrambe le parti siano presenti nello stesso momento (urlare, telefonare) ! comunicazione persistente : permette a mittente e ricevente di partecipare in momenti differenti (posta, e-mail) !la mutua esclusione richiede comunicazione persistente !il protocollo barattolo-corda ( can-and-string ) corrisponde ad un comune protocollo dei sistemi concorrenti: gli interrupt !nei sistemi operativi, un thread può attirare l’attenzione di un altro thread inviandogli un interrupt !un thread B interrompe il thread A settando un bit in una locazione che A controlla periodicamente !A reagirà all’interrupt e poi riporterà il bit al valore originale !gli interrupt non sono la soluzione alla mutua esclusione, ma restano importanti !la favola ci mostra che si può risolvere il problema della mutua esclusione con due variabili di booleane, ognuna delle quali può essere letta da un thread e scritta da un altro ( flag ) IL PROBLEMA DEL PRODUTTORE E CONSUMATORE !Supponiamo che Alice e Bob si sposino ma poi divorzino, mentre cane e gatto hanno nel frattempo imparato ad andare d’accordo !l’accordo di divorzio prevede che Alice mantenga gli animali (che attaccano Bob quando lo vedono) e Bob fornisca il cibo !il protocollo deve permettere a Bob di portare il cibo agli animali ( produttore ) quando essi non sono nel giardino ed ad Alice di rilasciare gli animali stessi ( consumatore ) solo quando c’è cibo, senza sprecare tempo !inoltre, Bob non vuole portare cibo se gli animali non hanno finito la razione precedente !per la soluzione di questo problema, entrambi decidono di usare il protocollo can-string !Bob posiziona una lattina alzata sul davanzale di Alice legandola ad una corda che arriva fino al suo salotto !Una volta messo il cibo, Bob tira la corda e fa cadere il barattolo !Da ora in poi, quando Alice vuole liberare gli animali:
!Bob, invece:
!Ogni algoritmo ha una componente sequenziale che limiterà eventualmente lo speedup ottenibile su un sistema parallelo ! Speedup = rapporto tra il tempo di esecuzione su un singolo processore ed il tempo di esecuzione su n processori !Legge di Amdahl: se la componente sequenziale di un algoritmo influisce 1/s sul tempo di esecuzione del programma, lo speedup Massimo possibile che può essere ottenuto su un computer parallelo è s ! esempio : se la componente sequenziale è il 5%, lo speedup massimo ottenibile è 20 !inizialmente, si pensava che questo effetto potesse limitare l’utilità della programmazione parallela ad un numero ristretto di applicazioni specializzate, ma poi è apparso chiaro che quasi tutti i problemi computazionali ammettono soluzioni parallele ! la scalabilità di alcune soluzioni può essere limitata, ma ciò è dovuto ai costi di comunicazione !La legge di Amdahl può essere rilevante quando programmi sequenziali sono parallelizzati incrementalmente !secondo questo approccio di sviluppo di sw parallelo, in un programma sequenziale vengono identificate le componenti parallelizzabili, le quali vengono adattate all’esecuzione parallela, una ad una, fino al raggiungimento di performances accettabili !la Legge di Amdahl si applica chiaramente a questa situazione perché i costi computazionali delle componenti non parallelizzate fornisce un lower bound al tempo di esecuzione del programma parallelo ! Problemi “culturali” !spesso le descrizioni degli algoritmi paralleli caratterizzano le performances con affermazioni che quasi non hanno senso e non danno informazioni utili !”Abbiamo implementato l’algoritmo su un computer parallelo X e abbiamo ottenuto uno speedup di 10.8 su 12 processori con taglia del problema N=100” !uno speedup di 10.8 su 12 processori può o non può essere considerate “buono” !comunque, una sola misura di performance (o addirittura alcune misure) serve solo a determinare le performance in una regione ristretta di uno spazio multidimensionale ed è spesso un indicatore misero in altre situazioni !cosa succede su 1000 processori? E cosa se N=10 o N=1000? Cosa succede se i costi di comunicazione sono 10 volte superiori?! per rispondere a queste domande è richiesta una conoscenza più profonda dell’algoritmo parallelo !Consideriamo le tre equazioni seguenti !Ognuno di esse è un semplice modello di performance che specifica il tempo di esecuzione T come funzione del numero di processori P e della taglia del problema N !In ogni caso, assumiamo che la computazione totale eseguita da un algoritmo sequenziale ottimale è N+N^2 !1. T = N + N^2 /P !questo algoritmo partiziona la componente O(N^2 ), ma replica su ogni processore la componente O(N) !Non ci sono altre fonti di overhead !2. T = (N + N2)/P +100! questo algoritmo partiziona tutta la computazione, ma introduce un costo addizionale di 100 !3. T = (N + N2)/P + 0.6P^2! questo algoritmo partiziona tutta la computazione, ma introduce un costo addizionale di 0.6P^2 !tutti e tre questi algoritmi ottengono uno speedup di circa 10.8 quando P=12 e N= !comunque, essi si comportano differentemente in altre situazioni !quando N=100 , tutti e tre gli algoritmi hanno basse performances per alti valori di P, sebbene l’algoritmo3 faccia notevolemte peggio degli altri due !quando N=1000 , l’algoritmo2 è significativamente migliore dell’algoritmo1 per valori alti di P ! Analisi asintotica !spesso i libri caratterizzano le performances degli algoritmi paralleli con qualcosa del tipo: !”L’analisi asintotica rivela che l’algoritmo richiede O(N logN) su O(N) processori”, cioè esistono una costante c e una taglia minima del problema N 0 tali che per ogni N > N 0 , cost(N) ≤ cN logN su N processori !questa relazione dice quanto il costo varia con N quando N e P sono grandi !sebbene interessante, questa info spesso non è direttamente rilevante per il lo sviluppo di un programma parallelo efficiente !siccome tratta con valori grandi di N e P, essa ignora termini “lower-order” che potrebbero essere significativi per taglia del problema e numero di processori di interesse pratico segue !!!!
! Continuo Analisi asintotica ! esempio: il costo effettivo di un algoritmo con una complessità asintotica di N logN potrebbe essere 10N + N logN !la componente 10N è più grande di N logN per N<1024 e deve essere incorporato in un modello di performances !una seconda mancanza dell’analisi asintotica è che non dice niente riguardo ai costi assoluti !l’analisi asintotica suggerirebbe che un algoritmo con costo 1000N logN è superiore ad un algoritmo con costo 10N^2 !il secondo è tuttavia più veloce per N<996 , che ancora potrebbe essere di interesse pratico !spesso l’analisi assume modelli di macchina idealizzati che sono ben distanti dai computer veri e propri per i quali sviluppiamo programmi !esempio: l’analisi può assumere la presenza di un modello PRAM, nel quale i costi di comunicazione sono nulli JAVA E LA CONCORRENZA !Java usa un modello di concorrenza nel quale i thread e gli oggetti sono entità separate ! Threads !Un thread esegue un singolo programma sequenziale !in Java un thread è solitamente una sottoclasse di java.lang.Thread , la quale fornisce metodi per la creazione di thread, per avviarli, sospenderli, ed attenderne la terminazione. !Per prima cosa, bisogna creare una classe che implementi l’interfaccia Runnable !tutto il lavoro è fatto dal metodo run() della classe !esempio: thread che stampa una stringa !un oggetto Runnable può essere eseguito in un thread chiamando il costruttore della classe Thread che prende un oggetto Runnable come argomento !un altro modo prevede di chiamare una classe interna anonima : !per eseguire un thread bisogna invocare il metodo thread.start() !il thread che chiama questo metodo ritorna immediatamente, senza aspettare che il chiamato inizi effettivamente l’esecuzione !per fare in modo che il chiamante aspetti che il thread finisca, bisogna invocare il metodo thread.join() !in questo modo, il chiamante è bloccato finchè il metodo run() del thread non ritorna !esempio: il seguente frammento mostra un metodo che inizializza thread multipli, li avvia, attende la loro terminazione e stampa un messaggio ! !dapprima viene creato un array di thread, inizializzati nelle linee 2-10, usando la sintassi per la anonymous inner class !alla fine del ciclo, è stato creato un array di thread dormienti !nelle linee 11-13, vengono avviati i thread ed ognuno di essi esegue il suo metodo, mostrando il suo messaggio !nelle linee 14-16, il metodo main attende che ciascun thread sia terminato e mostra un messaggio ! Monitors !Java fornisce vari modi per sincronizzare l’accesso a dati condivisi, sia built-in che all’interno di packages !I monitor sono l’approccio built-in più semplice e più comunemente usato !esempio: immaginiamo di avere in carico un software per un call center !durante l’ora di punta, le chiamate arrivano più velocemente del tempo di risposta segue !!!!
! Continuo Oggetti Thread-Local !la classe ThreadLocal
! Continuo Sezioni critiche !diciamo che un thread acquisisce un lock quando chiama il metodo lock(), e rilascia il lock quando chiama unlock() !La classe Counter, utilizzando un campo di tipo Lock per aggiungere mutua esclusione, diventa: !i thread che usano i metodi lock() e unlock() devono seguire uno specifico formato !un thread è ben formato se:
! Continuo Algoritmo di Peterson !Lemma: L’algoritmo del lock di Peterson soddisfa la mutua esclusione ! Prova: !Per assurdo, supponiamo che Peterson non soddisfi la mutua esclusione !Consideriamo l’ultima esecuzione del metodo lock() dei thread A e B !Dal codice ( 1 ) writeA(flag[A] = true) → writeA(victim = A) → readA(flag[B]) → readA(victim) → CSA ( 2 ) writeB(flag[B] = true) → writeB(victim = B) → readB(flag[A]) → readB(victim) → CSB !Assumiamo, senza perdere in generalità, che A sia stato l’ultimo thread a scrivere nel campo victim, dunque ( 3 ) writeB(victim = B) → writeA(victim = A) !ciò implica che A ha visto che la vittima è A nell’equzione (1) !siccome A tuttavia è entrato nella sua sezione critica, deve aver trovato il flag [B] = false, da cui ( 4 ) writeA(victim = A)→readA(flag[B] == false) !la (2), la (3) e la (4), insieme alla transitività dell’operazione →, implicano writeB(flag[B] = true)→writeB(victim = B)→ writeA(victim = A)→readA(flag[B] == false) !ne segue che writeB(flag[B] = true) → readA(flag[B] == false) !questa osservazione produce una contraddizione, dato che nessun’altra scrittura a flag[B] è stata fatta prima delle esecuzioni della sezione critica ■ !Lemma: L’algoritmo del lock di Peterson starvation-free ! Prova: !Per assurdo, supponiamo che Peterson non soddisfi la starvationm-freedom !Supponiamo (senza perdita di generalità) che A esegua per sempre il metodo lock() !In questo caso, deve essere eseguita l’istruzione while(), aspettando finché flag[B] diventi falsa o victim sia settata a B !mentre A non riesce ad andare avanti, forse B entra ed esce ripetutamente dalla sezione critica !ma se è così, B setta victim a B appena rientra nella sezione critica !una volta che victim è settato a B, non cambia e A deve eventualmente uscire dal metodo lock(), generando una contraddizione. !Quindi deve essere che anche B è bloccato nella sua chiamata al metodo lock(), aspettando finché il valore di flag[A] non diventi falso o victim sia settato ad A !Ma victim non può essere contemporaneamente A e B, quindi anche qui c’è una contraddizione ■ !Corollario: L’algoritmo di lock di Peterson deadlock-free CORRETTEZZA (intesa come FAIRNESS) !La starvation-freedom garantisce che ogni thread che chiama lock() eventualmente entra nella sezione critica, ma non garantisce in quanto tempo lo farà !Idealmente, se A chiama lock() prima di B, allora A dovrebbe entrare nella sezione critica prima di B !Sfortunatamente non è possibile determinare quale thread chiama il lock() per primo !Dividiamo il metodo lock() in due sezioni di codice: !( 1 ) sezione doorway, in cui l’intervallo di esecuzione DA consiste in un limitato numeri di step [requisito molto forte] !( 2 ) una sezione waiting , in cui l’intervallo di esecuzione WA può impiegare un numero di step illimitato ! Def.: Un lock è first-come-first-served se, ogni volta che un thread A finisce la sua parte doorway prima che il thread B inizi la sua
j
k
j
k B ALGORITMO DEL FORNAIO (di Leslie Lamport) ! !questo algoritmo mantiene la proprietà di first-come-first-served usando una versione distribuita delle macchine che distribuiscono numeri nelle panetterie: ogni thread prende un numero nella doorway, attende finchè nessun thread con un numero precedente sta provando ad entrare in sezione critica ed infine vi entra esso stesso ! flag[A] = flag booleano che indica se A vuole entrare nella sezione critica ! label[A] = intero che indica l’ordine relativo del thread quando entra nella panetteria (il “numeretto” prelevato) !ogni volta che un thread acquisisce un lock, genera una nuova label[] in due passi. !( 1 ) legge tutte le altre label dei thread in qualsiasi ordine !( 2 ) legge tutte le altre label dei thread, una dopo l’altra e genera una label maggiore della label massima letta !Le linee 13 e 14 rappresentano la parte doorway e stabiliscono l’ordine del thread nell’acquisizione dei lock
! Lemma: Nessun algoritmo di Lock deadlock-free può entrare in uno stato inconsistente !Dim: Supponiamo che l’oggetto Lock è in uno stato inconsistente s (nessun thread è in sezione critica, nè sta cercando di entrarvi) !se il thread B cerca di entrare in sezione critica, deve avere successo perché l’algoritmo è deadlok-free !Supponiamo che l’oggetto Lock è in uno stato inconsistente s, Dove A è in sezione critica !se B cerca di entrare in sezione critica, si deve bloccare finchè A ne esce !Abbiamo una contraddizione perché B non può determinare se A è in sezione critica ■ !qualsiasi algoritmo di Lock che risolve la mutua esclusione deadlock-free deve avere n locazioni distinte !Un covering state per un oggetto Lock è uno stato in cui c’è almeno un thread in procinto di scrivere su ogni locazione condivisa, ma le locazioni dell’oggetto Lock fanno sembrare la sezione critica vuota, cioè il loro stato appare come se non ci fosse nessun thread né in sezione critica, né in procinto di entrarci !in un covering state, ogni thread copre la locazione in cui sta per scrivere ! Teorema: qualsiasi algoritmo di lock deadlock free che leggendo e scrivendo memoria risolve la mutua esclusione per tre thread, deve usare almeno tre locazionidistinte !Dim: Assumiamo per contraddizione di avere un algoritmo deadlock free per tre thread con solo due locazioni ! inizialmente, nello stato s nessun thread è in sezione critica, nè sta cercando di entrarvi !se facciamo girare un qualsiasi thread, allora questo deve scrivere in almeno una locazione prima di entrare in sezione critica, altrimenti s è uno stato inconsistente !Ne segue che ogni thread deve scrivere in almeno una locazione prima di entrare !Se le locazioni condivise sono di tipo single-writer come nel Bakery Lock, è immediato il fatto che sono necessarie tre locazioni distinte (abbiamo tre thread!) !Consideriamo ora delle locazioni multiwriter come la locazione victim nell’algoritmo di Peterson !Sia s un covering Lock state dove A e B coprono locazioni diverse !Consideriamo la seguente esecuzione a partire da uno stato s !Supponiamo che C giri da solo !Siccome l’algoritmo di Lock soddisfa la proprietà di deadlock- freedom, alla fine C entra in sezione critica !Supponiamo che A e B aggiornino le rispettive locazioni “coperte”, lasciando l’oggetto Lock nello stato s’ !Lo stato s’ è inconsistente perché nessun thread può dire se C è in sezione critica, quindi un lock con due locazioni è impossibile !Rimane da mostrare come manovrare i thread A e B in un covering state !Consideriamo un’esecuzione in cui B gira in sez critica 3 volte !Ogni volta, B deve scrivere alcune locazioni, quindi consideriamo la prima locazione in cui scrive quando cerca di entrare in sezione critica !Siccome ci sono solo due locazioni, B deve scrivere due volte in una stessa locazione, che chiamiamo LB !Supponiamo che B stia girando in attesa di poter scrivere in LB per la prima volta !Se A venisse eseguito ora, entrerebbe in sezione critica perché B non ha ancora scritto niente !A deve scrivere LA prima di entrare in sezione critica, altrimenti, se scrive solo LB, supponiamo che A entra in sezione critica e che B scrive LB (“cancellando” l’ultima scrittura di A ) !il risultato è uno stato inconsistente: B non può dire se A è in sezione critica ! Supponiamo che A stia girando in attesa di poter scrivere in LA per la prima volta !Questo non è un covering state perché A potrebbe aver scritto qualcosa in LB indicando al thread C che sta cercando di entrare in sezione critica !Supponiamo che B stia girando, cancellando qualsiasi valore che A potrebbe aver scritto in LB, entrando e lasciando la sezione critica al più tre volte, e fermandosi appena prima della seconda scrittura in LB !Notiamo che ogni volta che B entra e lascia la sezione critica, se ha scritto nelle locazioni non è più importante !In questo stato, A sta per scrivere LA, B sta per scrivere LB e le locazioni non sono consistenti con nessun thread che sta cercando di entrare in sezione critica, come richiesto in un covering state e come illustrato nelle figura a lato ■
1 class LockBasedQueue
!lo stesso tipo di argomentazione può essere usata per mostare che la mutua esclusione deadlock-free per n-thread richiede n locazioni distinte !il lock di Peterson ed il Bakery lock sono ottimali (in un fattore costante) !Comunque, la necessità di allocare n locazioni per Lock li rende impraticabili !Questa prova mostra il limite delle operazioni di read e write: le informazioni scritte da un thread potrebbero essere sovrascritte senza che nessun altro thread le abbia lette OGGETTI CONCORRENTI !Il comportamento degli oggetti concorrenti è descritto nel modo migliore attraverso le loro proprietà di safety e liveness, spesso chiamate correttezza e progresso CONCORRENZA E CORRETTEZZA !coda FIFO lock-based !gli elementi della coda sono mantenuti in un array, dove head è l’indice del prossimo elemento per il dequeue, e tail è l’indice del primo slot dell’array libero !il campo lock assicura che i metodi siano mutuamente esclusivi !inizialmente head e tail valgono 0 e la coda è vuota !se enq () trova la coda piena, cioè la differenza tra head e tail è pari alla dimensione della coda, allora viene lanciata un’eccezione !altrimenti enq () inserisce l’elemento in coda ed incrementa tail !il metodo deq () funziona in modo simmetrico !questa implementazione è una coda FIFO corretta !dato che ogni metodo accede ed aggiorna campi mantenendo un lock esclusivo, le chiamate a metodi hanno effetto in modo sequenziale ! !A inserisce a, B inserisce b e C fa due volte deque(), la prima lanciando una EmptyException, la seconda restituendo b !La sovrapposizione di intervalli indica chiamate a metodo !le linee scure indicano intervalli !gli intervalli per un singolo thread sono mostrati lungo una singola linea orizzontale !Una barra rappresenta un intervallo con un tempo definito sia di start, sia di stop !Una barra con linee tratteggiate sulla destra rappresenta un intervallo con un tempo di start fissato e un tempo di stop sconosciuto ! q.enq(x) significa che un thread accoda l’elemento x all’oggetto q, mentre “ q.deq(x)” significa che il thread toglie x dalla coda q !nello specifico, C acquisisce il lock, osserva che la coda è vuota, rilascia il lock e lancia un’eccezione (senza modificare la coda) !B acquisisce il lock, inserisce b e rilascia il lock !A acquisisce il lock, inserisce a e rilascia il lock !C riacquisisce il lock, toglie b dalla coda, rilascia il lock e termina !Ognuna di queste chiamate ha effetto in modo sequenziale segue !!!!