Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Pcp concorrente, Appunti di Algoritmi E Programmazione Avanzata

appunti di programmazione concorrente

Tipologia: Appunti

2015/2016

Caricato il 02/08/2016

Syloader
Syloader 🇮🇹

5

(1)

2 documenti

1 / 73

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
1
PROGRAMMAZIONE CONCORRENTE E PARALLELA (PARTE DI PROGRAMMAZIONE CONCORRENTE)
INTRODUZIONE
!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-Set
!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;
}
}
}
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f
pf20
pf21
pf22
pf23
pf24
pf25
pf26
pf27
pf28
pf29
pf2a
pf2b
pf2c
pf2d
pf2e
pf2f
pf30
pf31
pf32
pf33
pf34
pf35
pf36
pf37
pf38
pf39
pf3a
pf3b
pf3c
pf3d
pf3e
pf3f
pf40
pf41
pf42
pf43
pf44
pf45
pf46
pf47
pf48
pf49

Anteprima parziale del testo

Scarica Pcp concorrente e più Appunti in PDF di Algoritmi E Programmazione Avanzata solo su Docsity!

PROGRAMMAZIONE CONCORRENTE E PARALLELA (PARTE DI PROGRAMMAZIONE CONCORRENTE)

INTRODUZIONE

! 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 INTRODUZIONE

! 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

  • da una piattaforma ad un’altra! i processori che gestiscono tostapane hanno cicli più lunghi rispetto a quelli che gestiscono Web server !i processori sono componenti hardware che eseguono thread ! cambio di contesto : un processore esegue un thread per un po’, per poi metterlo da parte ed eseguire un altro thread !un thread può essere “messo da parte” per diversi motivi ! es: * ha emesso una richiesta di memoria che necessita di molto tempo per essere soddisfatta
  • è stato eseguito per molto tempo ed è il momento di eseguire un altro thread !quando un thread viene cancellato dalla lista di un processore può riprendere l’esecuzione su un altro processore ! Interconnessione !mezzo attraverso il quale i processori comunicano con la memoria e con gli altri processori !L’interconnessione avviene tramite un mezzo di comunicazione chiamato bus !Sia il processore che il controller della memoria possono trasmettere dati sul bus, ma un solo processore per volta può trasmettere, mentre tutti gli altri possono ascoltare !Le interconnessioni possono essere essenzialmente di 2 tipi ! SMP (Symmetric Multi Processing) !i processori e la memoria sono collegati da un bus !sia i processori sia la memoria hanno un bus controller, ognuno incaricato di inviare ed ascoltare i messaggi in broadcast sul bus [questo ascolto è detto snooping ] ! vantaggio : SMP è molto semplice da realizzare ! svantaggio : poco scalabile: con un numero elevato di processori il bus risulta sovraccarico ! NUMA (Non Uniform Memory Access) !una collezione di nodi è collegata da una rete punto-punto, come una piccola LAN !ogni nodo contiene uno o più processori ed una memoria locale, ma può accedere alla memoria degli altri nodi !l’accesso alla memoria è non uniforme, quindi il tempo per l’accesso alla memoria locale da parte di un processore è più veloce dell’accesso ad una memoria che risiede su un altro nodo !questa architettura risulta essere più scalabile in relazione all’aumentare del numero di processori !ovviamente esistono delle architetture ibride , in cui i processori dove i processori all’interno di un cluster comunicano tramite un bus e i processori in cluster diversi comunicano tramite una rete !l’interconnessione è una risorsa finita condivisa tra i processori: se un processore usa troppo la banda dell’interconnessione, gli altri possono essere ritardati ! Memoria !i processori condividono una memoria principale, vista come un grande array di parole, indicizzate per indirizzo !in base alla piattaforma, una parola è tipicamente di 32 o 64 bit ! Lettura! è effettuata tramite l’invio di un messaggio dal processore alla memoria, con l’indirizzo da leggere come parametro !la memoria risponde inviando il valore associato a quell’indirizzo ! Scrittura !un processore scrive un valore inviando l’indirizzo e il nuovo valore alla memoria !la memoria risponde con un ack quando il nuovo valore viene effettivamente memorizzato

CONTINUO CACHES

! 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 ARCHITETTURE MULTI-CORE E MULTI-THREAD

! Continuo Consistenza rilassata della memoria ! Vantaggi : * batching: è spesso più efficiente emettere più richieste tutte in una volta

  • write obsorption : se un thread scrive più volte nello stesso indirizzo di memoria, solo l’ultima operazione viene effettivamente eseguita in memoria, cioè le precedenti scritture vengono assorbite ! Conseguenza: l’ordine con cui vengono richieste read/write in memoria non è lo stesso con cui realmente avvengono !I compilatori complicano ancora di più la situazione. !un compilatore può ottimizzare il codice effettuando un reordering delle read/write in memoria, ottenendo una buona ottimizzazione delle performances su architetture a singolo processore !il reordering è invisibile ai programmi single-thread, ma può avere conseguenze inaspettate per programmi multi-thread nei quali i thread potrebbero tenere conto dell’ordine in cui avvengono le scritture ! es : può accadere che un thread riempia un buffer e poi setti un flag per indicare che è pieno !il reordering potrebbe fare in modo che gli altri thread vedano il flag di buffer pieno quando ancora questi non lo è e leggano dati obsoleti !tutte le architetture permettono di forzare le scritture nell’ordine in cui sono state emesse, ma ad un costo ! Memory barriers (o fences): effettuano il flush dei buffer di scrittura !tutte le scritture emesse prima della barriera diventano visibili al processore che ha lanciato la barriera stessa !spesso le barriere sono inserite in modo trasparente da operazioni atomiche read-modify-write come getAndSet (), o da librerie standard concorrenti !l’uso esplicito è necessario solo quando i processori eseguono istruzioni di lettura-scrittura su variabili condivise al di fuori della sezione critica !Compromesso: *da una parte le barriere sono costose! andrebbero usate solo quando necessario *dall’altra i bug di sincronizzazione possono essere difficili da rintracciare !le barriere andrebbero usate liberamente, senza basarsi su garanzie specifiche della piattaforma sui limiti al reordering delle istruzioni in memoria !Java permette che le letture-scritture ai campi di oggetti possano essere riordinate se al di fuori di metodi o blocchi synchronized !in Java la parola chiave volatile assicura che le letture-scritture ad un campo di un oggetto volatile, che avvengono al di fuori di blocchi o metodi synchronized, non siano riordinate ! volatile è usata per indicare che il valore della variabile sarà modificato da thread diversi, non sarà mai cachato da un singolo thread e l’accesso ad essa avverrà come se fosse racchiusa in un blocco synchronized !l’uso di questa parola chiave può essere costoso, quindi andrebbe usata solo quando necessario OGGETTI CONDIVISI E SINCRONIZZAZIONE !I sistemi basati su multiprocessore spesso sono chiamati anche shared-memory multiprocessors oppure semplicemente multicores

!ogni chip deve essere coordinato per poter accedere alle locazioni di memoria condivisa e su larga scala

!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 UNA FAVOLA

! 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:

  1. attende fin quando la lattina sia giù;
  2. rilascia gli animali;
  3. quando questi tornano, controlla che hanno finito il cibo; se è così, resetta la lattina rimettendola in piedi segue !!!!

CONTINUO PROBLEMA DEL PRODUTTORE E CONSUMATORE

!Bob, invece:

  1. aspetta che la lattina sia in piedi;
  2. mette il cibo nel giardino;
  3. tira la corda e abbassa la lattina. ! la lattina è una variabile di 1 bit che può essere scritta e letta da entrambi (nell’esempio Bob scrive solo 0 mentre Alice scrive solo 1) !anche in questo caso sono presenti i problemi di incremento atomico !lo stato della lattina riflette lo stato del cibo: se è giù (1) c’è cibo, se è su (0) il cibo non c’è e Bob può metterne altro !il protocollo gode di tre proprietà !Mutua esclusione: Bob e gli animali non sono mai nel giardino insieme !Starvation-freedom: Se Bob mette sempre del cibo e gli animali sono sempre affamati, allora gli animali mangeranno all’infinito (non esiste un limite al numero di volte in cui il protocollo può essere ripetuto) !Produttore-consumatore: gli animali non entreranno mai nel giardino a meno che non ci sia del cibo e Bob non fornirà mai altro cibo se ce n’è già !il protocollo Produttore-Consumatore non richiede deadlock freedom !Alice e Bob, comunque, non possono usare questo protocollo Produttore-Consumatore per la mutua esclusione, in quanto quest’ultima richiede deadlock-freedom: ognuno deve essere in grado di entrare nel luogo in comune anche se l’altro non è in casa !Dimostrazione che il protocollo soddisfa le tre proprietà sopra ! Mutua esclusione: Rappresentiamo la lattina come una macchina a due stati: down e up !Dimostriamo che in tutti gli stati la proprietà di mutua esclusione è mantenuta !Supponiamo che all’inizio la lattina sia down: in questo caso gli animali possono entrare ma Bob no! proprietà vera !Dallo stato down, la lattina può passare allo stato up, ma solo se Alice ha fatto uscire dal giardino gli animali e Bob può entrarvi (proprietà vera) ! Starvation-freedom: Supponiamo che la proprietà non sia soddisfatta !È il caso in cui gli animali di Alice sono sempre affamati, non c’è cibo e Bob sta cercando di recuperarlo, ma senza riuscirci !La lattina non può essere up, finché Bob fornirà il cibo e tirerà la corda, permettendo agli animali di mangiare !Per questo, la lattina deve essere down e finché gli animali sono affamati, Alice alzerà la lattina, tornando al caso precedente ! Produttore-Consumatore: La mutua esclusione implica che gli animali e Bob non saranno mai insieme nel giardino !Bob non vi entrerà fino a quando Alice non rialzerà la lattina e Alice lo farà solo quando non ci sarà più cibo !Gli animali non entreranno nel giardino prima che Bob abbia abbassato la lattina e Bob lo farà solo dopo che avrà depositato il cibo nel giardino. !Anche questo protocollo mostra waiting: se Bob deposita il cibo nel giardino e immediatamente va in vacanza dimenticandosi di resettare la lattina, gli animali possono morire di fame, nonostante la presenza di cibo. !il problema del Produttore-Consumatore è il modo in cui i processori depositano dati nei buffer di comunicazione condivisi IL PROBLEMA DEI LETTORI-SCRITTORI !Supponiamo che Alice e Bob vogliano comunicare dopo il divorzio !Bob mette una grande lavagna fuori casa sua e si serve di essa per comunicare !la lavagna può contenere una sequenza di mattonelle, ognuna delle quali contiene una lettera !quindi Bob mette, una alla volta, le lettere che compongono il messaggio e Alice può leggerle !Supponiamo, però, che Bob scriva “ sell the cat” e che Alice trascriva il messaggio fino a “ sell the” !A questo punto Bob toglie le mattonelle e scrive il nuovo messaggio “ wash the dog”, ma Alice, continuando a scrutare la lavagna, trascriva il messaggio sell the dog !si possono immaginare le conseguenze !Soluzione1: Alice e Bob possono usare il protocollo di mutua esclusione per far si che Alice legga solo frasi complete !potrebbe comunque succedere che Alice perda qualche frase !Soluzione2: Alice e Bob possono usare il protocollo del produttore-consumatore can-string, in cui Bob produce frasi e Alice le consuma !sia la mutua esclusione che il protocollo produttore-consumatore richiedono waiting !Nel contesto dei multiprocessori a memoria condivisa, una soluzione sarebbe permettere ad un thread di catturare una vista istantanea di molte locazioni di memoria !il catturare per ciascuno una vista senza attendere, cioè senza prevenire la scrittura di queste locazioni da parte di altri thread, è uno strumento potente che può essere usato per backup, debugging, e in molte altre situazioni LE DURE REALTA’ DELLA PARALLELIZZAZIONE !in un mondo ideale, passando da un processore a n processori, ci dovrebbe essere un incremento di potenza computazionale pari a n !in pratica questo non accade !la ragione principale è che la maggior parte dei problemi computazionali reali non possono essere parallelizzati senza incorrere nel costo della comunicazione inter-processo e della coordinazione ( es : mutua esclusione) Segue !!!!

APPROFONDIMENTO SULLA LEGGE DI AMDAHL

!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 APPROFONDIMENTO SULLA LEGGE DI AMDAHL

! 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 JAVA E LA CONCORRENZA

! Continuo Oggetti Thread-Local !la classe ThreadLocal gestisce una collezione di oggetti di tipo T, uno per ogni thread !essa fornisce il metodo initialValue() per l’inizializzazione, chiamato ogni volta che un thread cerca di accedere al valore di un oggetto thread-local, ed i metodi get() and set() per leggere ed aggiornare il valore locale al thread !la classe ThreadLocal non è utilizzabile direttamente, ma bisogna definire una sottoclasse che sovrascrive initialValue() per inizializzare ogni oggetto nel modo appropriato ! esempio: !quando un thread chiama get() la prima volta, gli viene assegnato il prossimo identificatore disponibile !ogni chiamata successiva restituisce l’id del thread !linea2: dichiara un campo intero nextID che contiene il prossimo intero disponibile !linee3-7:classe interna che gestisce l’id del thread e che sovrascrive initialValue() in modo che assegni il successivo id non utilizzato al thread corrente ! La Java Virtual Machine e i thread !Nelle prime macchine virtuali (1.1, 1.2) su alcuni sistemi operativi si usavano i green thread (simulazione di thread) !adesso le macchine virtuali sono altamente ottimizzate per usare le capacità multi-thread dei sistemi operativi su cui si poggiano, rendendo immediato l’uso efficiente dei multiprocessori/multicore attualmente disponibili MUTUA ESCLUSIONE !La mutua esclusione è la forma prevalente di coordinazione nella programmazione multiprocessore ! Tempo !il tempo è un fattore fondamentale per il calcolo concorrente: abbiamo bisogno di un linguaggio semplice e non ambiguo per poter parlare di eventi e della loro durata temporale !i thread condividono un tempo comune !un thread è una macchina a stati e le transizioni di stato sono chiamati eventi !Gli eventi sono istantanei, ossia si verificano in un singolo istante di tempo !è conveniente richiedere che eventi distinti si verifichino in momenti distinti !un thread A produce sequenze di eventi a 0 , a 1 ,… !un thread può contenere cicli, quindi una singola istruzione di un programma può produrre diversi eventi !Indichiamo con "#$ la j-esima occorrenza di un evento ai !un evento a precede un altro evento b (a → b) se a si verifica prima di b !la relazione → è un ordine totale sugli eventi !una relazione d'ordine totale è una relazione binaria su un insieme X che è riflessiva, antisimmetrica, transitiva e totale !ciò significa che se denotiamo una tale relazione con ≤ valgono i seguenti enunciati per tutti gli a , b e c elementi di X : !per ogni a aa (riflessività) !se ab e ba , allora a = b (antisimmetria) !se ab e bc allora ac (transitività)! ab oppure ba (totalità) !Siano a 0 e a 1 eventi tali che a 0 → a 1 ; l’intervallo (a 0 , a 1 ) è la durata tra a 0 e a 1 !l’intervallo IA=(a 0 , a 1 ) precede l’intervallo IB=(b 0 , b 1 ), [ IAIB ], se a 1 → b 0 , ossia se l’evento finale di IA precede quello iniziale di IB !la relazione → è un ordine parziale sugli intervalli e gli intervalli che non sono relazionati da → vengono detti concorrenti ! Sezioni critiche !questa implementazione del Counter è corretta in un sistema a singolo thread, ma si comporta male se, quando usata da due o più thread, entrambi leggono il campo value (nella linea marcata “ start of danger zone ”) e poi aggiornano quel campo (nella linea marcata “ end of danger zone ”) ! possiamo evitare questo problema trasformando queste due linee in una sezione critica , cioè un blocco di codice eseguibile solo da un thread per volta [proprietà della mutua esclusione ] !è necessario definire un oggetto Lock che realizza l’interfaccia

CONTINUO MUTUA ESCLUSIONE

! 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:

  1. Ogni sezione critica è associata ad un unico oggetto Lock
  2. il thread chiama il metodo lock() di quell’oggetto quando prova ad entrare nella sezione critica
  3. il thread chiama il metodo unlock() quando lascia la sezione critica ! !NOTA: è da notare come i metodi lock() e unlock() sono stati usati in in modo strutturato (linee 6-12): in questo modo il lock è acquisito prima di entrare nel blocco try ed è rilasciato quando il controllo lascia il blocco !il rilascio avviene comunque, anche se viene lanciata un’eccezione nel blocco try ! Formalizzazione delle proprietà da soddisfare !Sia CS jA l’intervallo durante il quale A esegue la sezione critica per la j-esima volta !Assumiamo che ogni thread acquisisca e rilasci il lock infinitamente spesso, con altro lavoro che sta avendo luogo nello stesso momento ! Mutua esclusione: Sezioni critiche di thread differenti non si sovrappongono !Per i thread A e B e gli interi j e k si ha che CS kA → CS jB or CS jB → CS kA ! Deadlock Freedom: Se qualche thread aspetta per acquisire il lock, allora qualche altro dovrà riuscire ad acquisirlo !se il thread A chiama il metodo lock() ma non lo acquisisce mai, allora altri thread devono terminare un numero infinito di sezioni critiche ! Starvation Freedom (o lockout freedom): Ogni thread che tenta di acquisire il lock alla fine ci riesce !ogni chiamata a lock() prima o poi ritorna !la proprietà di starvation freedom implica la proprietà di deadlock freedom !Senza la mutua esclusione non possiamo garantire che i risultati di una computazione sono corretti [proprietà di sicurezza] !la proprietà di deadlock freedom implica che il sistema non si “congeli” mai [proprietà di liveness] !thread individuali potrebbero essere bloccati per sempre (starvation), mentre qualche thread continua a processare (quindi non c’è deadlock) !un programma può comunque andare in deadlock anche se ogni lock che usa soddisfa la deadlock freedom !esempio: consideriamo i thread A e B che condividono i lock α 0 e α 1 !dopo che A acquisisce α 0 e B acquisisce α 1 , A prova ad acquisire α 1 e B prova ad acquisire α 0 !i thread vanno in deadlock perché ognuno attende che l’altro rilasci il suo lock !la starvation freedom è la meno convincente delle tre, in quanto esistono algoritmi pratici di mutua esclusione che non la soddisfano (algoritmi usati quando la starvation è una possibilità teorica, ma è improbabile che accada realmente) !la starvation freedom, inoltre, è anche debole nel senso che non c’è garanzia riguardo a quanto tempo un thread attende prima di entrare nella sezione critica SOLUZIONI A 2 THREAD ! Convenzioni !i thread hanno id 0 o 1, il thread chiamante ha i e l’altro j=1-i !ogni thread acquisisce il suo indice chiamando ThreadID.get() ! La classe LockOne !le variabili booleane flag vanno dichiarate volatile per funzionare correttamente !con writeA(x = v) denotiamo l’evento in cui A assegna il valore v al campo x, mentre con readA(v = = x) denotiamo l’evento in cui A legge v dal campo x. !Nello specifico, solo A può scrivere il flag[A] e lo stesso vale per B, cioè non può accadere che writeA(flag[B] = newValue) !l’algoritmo prevede che un thread segnali la sua volontà ad entrare in sezione critica e, a meno che non ci sia un altro thread già in sezione critica, che vi entri Segue !!!!

CONTINUO SOLUZIONI A 2 THREAD

! 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

parte doorway, allora A non può essere superato da B, cioè if D

j

A →^ D

k

B, then CS

j

A →^ CS

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

  1. public interface Timestamp {
  2. boolean compare(Timestamp);
  3. }
  4. public interface TimestampSystem {
  5. public Timestamp[] scan();
  6. public void label(Timestamp timestamp, int i);
  7. } CONTINUO ALGORITMO DEL FORNAIO (di Leslie Lamport) !Problema: se due thread eseguono la loro doorway concorrentemente, possono leggere la stessa etichetta massima e scegliere la stessa label. !Soluzione: l’algoritmo usa un ordine lessicografico << sulle coppie (label[], thread-id) (label[i], i) << (label[j], j)) se e solo se label[i] < label[j] or label[i] = label[j] and i < j !alla linea 15 (parte di waiting), un thread legge ripetutamente le etichette una dopo l’altra in un ordine arbitrario prima di determinare che nessun thread in attesa di acquisire il lock abbia una coppia (label,id) lessicograficamente più piccola !dato che il rilascio del lock non resetta la label[], è facile vedere che le label dei thread sono rigorosamente crescenti !sia nella sezione doorway, sia in quella di waiting, i thread leggono le etichette asincronamente e in ordine arbitrario, cosi che l’insieme delle etichette visto prima della scelta della nuova può non essere mai esistito in memoria !tuttavia l’algoritmo funziona ! Lemma: L’algoritmo del fornaio è deadlock-free ! Prova: un qualche thread A in attesa ha l’unica coppia più piccola (label[A],A), e quel thread non attende mai un altro thread ■ ! Lemma: l’algoritmo del fornaio è first-come-first-served. ! Prova: se la doorway di A precede quella di B, DA →DB, allora l’etichetta di A è più piccola, dato che writeA(label[A]) → readB(label[A]) → writeB(label[B]) → readB(flag[A]) !quindi B è bloccato mentre flag[A] è true ■ !qualsiasi algoritmo deadlock-free e firt-come-first-served è anche starvation-free ! Lemma: L’algoritmo del fornaio soddisfa la mutua esclusione ! Prova : !Per assurdo, supponiamo che l’algoritmo del fornaio non soddisfi la mutua esclusione. !Siano A e B due thread che si trovano concorrentemente nella sezione critica. !Siano labelingA e labelingB le ultime sequenze di acquisizione di nuove etichette prima di entrare in sezione critica !Supponiamo che (label[A],A) << (label[B],B) !Quando B completa con successo il test nella sua sezione di waiting, deve aver letto che il flag[A] era false o che (label[B],B) << (label[A],A) !Tuttavia, per un dato thread, il suo id è fissato e i suoi valori label[] sono strettamente crescenti! B deve aver visto che il flag[A] era false !ne segue che labelingB→readB(flag[A])→writeA(flag[A])→labelingA, che contraddice (label[A],A) << (label[B],B) ■ TIMESTAMP LIMITATI !le etichette del lock del fornaio crescono senza limite, quindi in un sistema longevo potremmo doverci preoccupare dell’overflow !se il campo etichetta di un thread passa silenziosamente da un numero grande a zero, allora la proprietà di FCFS non sussisterà !è difficile generalizzare l’importanza del problema dell’overflow nel mondo reale !esempi: Y2K bug (noto anche come Millennium Bug); bug che il 18 Gennaio 2038 provocherà l’overflow della struttura time_t dei sistemi Unix a 32-bit !è possibile usare i contatori sia per ordinare thread, ma anche per produrre identificatori univoci, ma anche in questo caso sussiste in alcune architetture il problema dell’overflow !nel Bakery lock le label agiscono come dei timestamp: stabiliscono un ordine tra i thread contendenti !Informalmente, dobbiamo assicurare che se un thread prende una label dopo un altro thread, allora l’ultimo ha la label più alta !Ispezionando il codice per il Bakery lock, vediamo che un thread ha bisogno di due capacità !scan: leggere i timestamp degli altri thread !label: assegnarsi un timestamp più recente !Interfaccia Java per un sistema di timestamping: !il sistema di timestamping deve essere wait-free !concentriamoci sulla costruzione di un sistema di timestamping sequenziale, in cui i thread eseguono operazioni di scan-and-label una dopo l’altra, cioè come se fossero eseguiti usando la mutua esclusione !in altre parole, consideriamo solo le esecuzioni in cui un thread può eseguire una scansione delle label degli altri thread e subito dopo un assegnamento di una nuova label, e ogni sequenza del genere è un unico passo atomico !pensiamo ai range dei possibili timestamp come a nodi di un grafo direzionato ( grafo di precedenza ) !un arco dal nodo a al nodo b significa che a è un timestamp più recente di b !l’ordinamento dei timestamp non è riflessivo: non esiste un arco da un nodo a se stesso !l’ordinamento dei timestamp è antisimmetrico: se c’è un arco da a a b , non può essercene uno da b ad a !non è richiesto che l’ordinamento sia transitivo: può esserci un arco da a a b ed uno da b a c senza che ci sia un arco da a a c !possiamo vedere l’assegnamento di un timestamp ad un thread come il piazzamento di un token del thread sul nodo di timestamp !un thread esegue una scansione localizzando i token degli altri thread e assegna a se stesso un nuovo timestamp spostando il proprio token su un nodo a tale che c’è un arco da a ad ogni altro nodo di thread !in pratica, implementiamo questo sistema come un array di campi single-writer/multi-reader, dove l’elemento dell’array A rappresenta il nodo nel grafo in cui il thread A ha posto il suo token più recente segue!

CONTINUO LOWER BOUND SUL NUMERO DI LOCAZIONI UTILIZZATE

! 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 { 2 int head, tail; 3 T[] items; 4 Lock lock; 5 public LockBasedQueue( int capacity) { 6 head=0;tail=0; 7 lock = new ReentrantLock(); 8 items = (T[]) new Object [capacity]; 9 } 10 public void enq(T x) throws FullException { 11 lock.lock(); 12 try { 13 if (tail - head == items.length) 14 throw new FullException(); 15 items[tail % items.length] = x; 16 tail++; 17 } finally { 18 lock.unlock(); 19 } 20 } 21 public T deq() throws EmptyException { 22 lock.lock(); 23 try { 24 if (tail == head) 25 throw new EmptyException(); 26 T x = items[head % items.length]; 27 head++; 28 return x; 29 } finally { 30 lock.unlock(); 31 } 32 } 33 }

CONTINUO LOWER BOUND SUL NUMERO DI LOCAZIONI UTILIZZATE

!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 !!!!