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


Gestione dei Processi e dei Thread nei Sistemi Operativi: Appunti Dettagliati - Prof. Spa, Appunti di Sistemi Operativi

Una panoramica dettagliata sulla gestione dei processi e dei thread nei sistemi operativi, con un focus particolare sui meccanismi di sincronizzazione. Vengono esaminati concetti chiave come il context switch, la creazione e terminazione dei processi, e le diverse tecniche per la sincronizzazione, inclusi semafori e monitor. Inoltre, viene analizzato il problema del deadlock e le strategie per la sua prevenzione e gestione. Gli appunti offrono una guida completa per comprendere le sfide e le soluzioni nella gestione concorrente dei processi, fornendo esempi pratici e spiegazioni chiare dei concetti fondamentali. Si discute anche il modello a scambio di messaggi e le condizioni per lo stallo, offrendo una visione approfondita delle problematiche legate alla concorrenza nei sistemi operativi moderni. Questi appunti sono ideali per studenti universitari e professionisti del settore che desiderano approfondire le proprie conoscenze sui sistemi operativi e la gestione dei processi.

Tipologia: Appunti

2024/2025

In vendita dal 10/07/2025

fra-piersi
fra-piersi 🇮🇹

5

(1)

11 documenti

1 / 50

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Inizio unità 2
Processi e Thread
Processo= programma in esecuzione P = (C,S) dove C è il codice eseguibile mentre S è lo stato
dell’esecuzione dove sono inclusi vari registri come: program counter, altri registri della CPU…
Illustrazione della struttura tipica della memoria di un processo (immagine slide 5)
Text = contiene il codice eseguibile del programma
data = contiene le variabili globali e statiche inizializzate
heap = è l’area di memoria utilizzata per l’allocazione dinamica
stack = utilizzata per la gestione delle chiamate a funzione e delle variabili locali
La separazione di questi segmenti aiuta il sistema operativo a gestire la memoria in modo
efficiente e sicuro. In sostanza l’immagine mostra una mappa fondamentale di come un
programma vede e utilizza la memoria durante la sua esecuzione.
Ora vediamo il ciclo di vita di un processo dal momento in cui ne viene chiesta la creazione al
momento di terminazione. (guarda slide 7 per immagine)
Quando ne viene chiesta la creazione, il kernel, crea un descrittore dove mette delle
informazioni che sono l’ID (un numero) e setta lo stato del processo in NEW nessuna
risorsa se non il descrittore
la seconda cosa che fa il kernal è cercare un’area di memoria da allocare e una volta trovata il
processo assegna il suo stato a READY, pronto per essere mandato in esecuzione.
A questo punto nel descrittore troviamo tutti i registri messi a zero, incluso il PC. Mentre il
registro base e il registro limite vengono impostati con valori che aiuteranno a definire l’area
di memoria che il processo potrà utilizzare.
Il processo ora è stato creato, ha la sua memoria e il suo descrittore. È nello stato READY in
attesa di essere eseguito dalla CPU.
A questo punto entra in gioco lo scheduler, una parte del sistema operativo che ha il compito
di decidere quale dei processi nello stato READY debba essere il prossimo a ottenere l’uso
della CPU.
Lo scheduler prende una decisione e sceglie un processo fornendo l’indirizzo di memoria del
descrittore del processo selezionato.
Questa informazione (l’indirizzo del PCB) viene poi passata al dispatcher. Il suo ruolo è quello
di prendere il processo selezionato dallo scheduler e caricarlo sulla CPU in modo da
riprendere l’esecuzione. Carica i valori dei registri, inclusi quelli basi e quelli limiti e tutti quelli
della CPU, che erano stati salvato nel descrittore del processo, nei rispettivi registri fisici della
CPU. Assegna il bit di modo e una volta che tutti i registri sono stati ripristinati e il bit di modo
impostato, il processore salta all’indirizzo contenuto nel PC. Qui entra nella fase di RUNNING.
Due cose possono capitare quando il processo è in fase di RUNNING:
- Terminazione volontaria: è il caso più comune quando il processo ha terminato il suo
compito e deve informare il sistema operativo che ha finito e che non ha più bisogno
delle risorse. Per fare ciò il processo effettua una system call, ovvero una chiamata a
una funzione del kernel specificatamente progettata per gestire la terminazione dei
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

Anteprima parziale del testo

Scarica Gestione dei Processi e dei Thread nei Sistemi Operativi: Appunti Dettagliati - Prof. Spa e più Appunti in PDF di Sistemi Operativi solo su Docsity!

Inizio unità 2 Processi e Thread Processo= programma in esecuzione P = (C,S) dove C è il codice eseguibile mentre S è lo stato dell’esecuzione dove sono inclusi vari registri come: program counter, altri registri della CPU… Illustrazione della struttura tipica della memoria di un processo (immagine slide 5) Text = contiene il codice eseguibile del programma data = contiene le variabili globali e statiche inizializzate heap = è l’area di memoria utilizzata per l’allocazione dinamica stack = utilizzata per la gestione delle chiamate a funzione e delle variabili locali La separazione di questi segmenti aiuta il sistema operativo a gestire la memoria in modo efficiente e sicuro. In sostanza l’immagine mostra una mappa fondamentale di come un programma vede e utilizza la memoria durante la sua esecuzione. Ora vediamo il ciclo di vita di un processo dal momento in cui ne viene chiesta la creazione al momento di terminazione. (guarda slide 7 per immagine) Quando ne viene chiesta la creazione, il kernel, crea un descrittore dove mette delle informazioni che sono l’ID (un numero) e setta lo stato del processo in NEW  nessuna risorsa se non il descrittore la seconda cosa che fa il kernal è cercare un’area di memoria da allocare e una volta trovata il processo assegna il suo stato a READY, pronto per essere mandato in esecuzione. A questo punto nel descrittore troviamo tutti i registri messi a zero, incluso il PC. Mentre il registro base e il registro limite vengono impostati con valori che aiuteranno a definire l’area di memoria che il processo potrà utilizzare. Il processo ora è stato creato, ha la sua memoria e il suo descrittore. È nello stato READY in attesa di essere eseguito dalla CPU. A questo punto entra in gioco lo scheduler, una parte del sistema operativo che ha il compito di decidere quale dei processi nello stato READY debba essere il prossimo a ottenere l’uso della CPU. Lo scheduler prende una decisione e sceglie un processo fornendo l’indirizzo di memoria del descrittore del processo selezionato. Questa informazione (l’indirizzo del PCB) viene poi passata al dispatcher. Il suo ruolo è quello di prendere il processo selezionato dallo scheduler e caricarlo sulla CPU in modo da riprendere l’esecuzione. Carica i valori dei registri, inclusi quelli basi e quelli limiti e tutti quelli della CPU, che erano stati salvato nel descrittore del processo, nei rispettivi registri fisici della CPU. Assegna il bit di modo e una volta che tutti i registri sono stati ripristinati e il bit di modo impostato, il processore salta all’indirizzo contenuto nel PC. Qui entra nella fase di RUNNING. Due cose possono capitare quando il processo è in fase di RUNNING:

  • Terminazione volontaria: è il caso più comune quando il processo ha terminato il suo compito e deve informare il sistema operativo che ha finito e che non ha più bisogno delle risorse. Per fare ciò il processo effettua una system call, ovvero una chiamata a una funzione del kernel specificatamente progettata per gestire la terminazione dei

processi. (EXIT()) Una volta che il processo ha invocato la funzione, il controllo passa al kernel che imposta lo stato le processo a TERMINATED.

  • Terminazione involontaria: in alcuni casi, un processo può essere terminato da un altro processo o dal sistema stesso. Ad esempio, un utente potrebbe decidere di “killare” un processo. Indipendentemente da chi o cosa abbia richiesto l’interruzione, il risultato è che il processo in esecuzione viene sospeso e il suo stato viene impostato a TERMINATED. Ora che è andato in TERMINATED il kernel deve via via liberare le risorse assegnate a questo processo: 1) Chiusura dei file che il processo aveva aperto File = non sono solo i documenti veri e propri sul disco rigido, ma sono anche tutti quei dispositivi di input output, socket di rete, pipe per la comunicazione tra processi e molto altro. Il kernel consulta la tabella dei file aperti associata a quel processo specifico che si trova nel suo PCB.
  1. Deallocazione della memoria Il kernel procede a recuperare la memoria RAM che era stata assegnata al processo. Questa memoria torna a essere libera e disponibile per essere assegnata a nuovi processi.
  2. Rimozione del descrittore di processo (PCB) Finito la rimozione del descrittore del processo, la vita del nostro processo termina. (immagine del descrittore del processo slide 8) Context Switch Il Context Switch rappresenta un cambio di contesto. Permette al computer di sembrare che stia eseguendo più programmi contemporaneamente, anche se, come sappiamo, una singola CPU può seguire solo un’istruzione alla volta. Un interrupt o una system call sono gli eventi scatenanti di un cambio di contesto. Supponiamo che il processo P0 sia in esecuzione sulla CPU; quindi, tutte le informazioni relative al suo stato sono caricate e attive. Immaginiamo ci sia un’interruzione, dove lo stato del nostro processo attuale P0 viene salvato nel suo PCB. Il sistema operativo carica il PCB del nuovo processo P1, il quale esegue le sue operazioni fino a quando non riceve una nuova interruzione. A questo punto il kernel decide di assegnare nuovamente la CPU al P0 che riprende a sua volta da dove era stato interrotto grazie al suo PCB. Da punto di vista del kernel il processo è un tipo di dato astratto, una classe, e ogni volta che viene creato un nuovo processo esso è un’istanza di quella classe dove gli attributi sono le informazioni che si trovano nel descrittore del processo. Il kernel fornisce anche dei metodi che possono essere invocati su questi oggetti. Il primo metodo di classe è il metodo costruttore che mi permette di creare un’istanza di quella classe. Questo crea un vero e proprio albero genealogico poiché ogni processo crea a sua volta un altro processo.
  • PROCESS BUILDER, che si interfaccia con il sistema operativo e fa le system call;
  • PROCESS, è l’equivalente del nostro descrittore dei processi. Slide 16 programma per fare questa cosa. Per primo mi creo un’istanza di process builder (gli dico anche qual è il programma che voglio mandare in esecuzione, quei parametri gli servono per descrivere il programma da mandare in esecuzione). Tra i vari metodi ce ne è uno che si chiama START, posso con questo creare un’istanza della classe process che ha assegnato ad una variabile che ha chiamato process. L’istanza di porcess non l’ho creata invocando direttamente il costruttore, ma l’ho fatto usando un metodo di un altro oggetto. (in realtà non è che posso mettere dentro il costruttore di process tutte le cose che mi servono per il processo, quindi meglio metterlo dentro un metodo assestante). Fino a lezione 8 unità 2 I thread e le operazioni sui thread immagine slide 12 appunti Un thread è un’unità di esecuzione all’interno di un processo, un processo può contenere uno o più thhread. Ogni processo è caratterizzato dalla parte di memoria e dalle risorse (parte in grigio). Per ora consideriamo le risorse come file. Un insieme di thread hanno in comune il codice, i file e i dati. Lo spazio di indirizzamento è condiviso (viene usata la stessa coppia di registri base e limite per tutti i thread di un processo).
  • Thread a livello utente : esempio: Java Virtual Machine
  • Thread a livello kernel : esempio: Windows Multithread IL multithread è la capacità di un singolo programma di eseguire più thread di esecuzione contemporaneamente. In altre parole, permette all’applicazione di svolgere diversi compiti apparentemente o realmente in parallelo. Aiuta la prontezza, condivisione delle risorse, economicità e utilizzazione di architetture multiprocessore. Ogni thread ha i suoi tra stati, i suoi registri inclusi, ma non vi è protezione tra di loro e questo potrebbe causare problemi ai dati. Modelli di multithreading MOLTI A UNO : slide 23. Ovvero avrò molti thread a livello utente, mappati ad un singolo thread a livello kernel. Viene usato nei sistemi che non forniscono thread a livello utente. PRO : tempo di context switch molto basso poiché non c’è il passaggio a kernel mode. Può essere implementato al di sopra del sistema operativo. CONTRO : è complicato fare in modo che un thread, che ha bisogno di una system call bloccante, non vada a bloccare anche gli altri thread di uno stesso processo. Solo un thread running per processo anche se multiprocessore.

UNO A UNO : slide 24. Ogni thread a livello utente si mappa a un thread a livello kernel. PRO : Multiprocessore con più thread running per processo e le system call bloccanti non pongono problemi, il sistema è in grado di schedulare più thread. CONTRO : Context switch fra thread è costoso perché richiede il passaggio a modalità kernel e il sistema operativo deve predisporre di strutture per la memorizzazione e gestione di tutti i thread, ma questo può essere limitativo. (esempio: linux) MOLTI A MOLTI : slide 25. Diversi thread a livello utente sono mappati a diversi thread a livello kernel, questo permette al sistema operativo di creare un numero sufficiente di thread a livello kernel. PRO : In sistemi multiprocessore è possibile avere più thread running per processo, il context switch fra thread a livello utente avviene in modalità utente, a cura del sistema runtime. Il sistema operativo effettua il context switch di kernel threads che risulterà un po’ meno costoso perché queste condivideranno lo spazio di indirizzi e quindi la tabella delle pagine. Il sistema operativo è in grado di schedulare i kernel threads pronti di un dato processo anche se una o più altri kernel threads dello stesso processo sono bloccatti in attesa di un evento. CONTRO : Il modello è più complicato e richiede che lo scheduler della CPU e lo scheduler dei threads a livello utente collaborino. Slide27 il codice qui sotto vuole illustrare la concorrenza in Java, mandando in esecuzione lo stesso codice svariate volte. Possiamo notare che il comportamento risulta diverso, nonostante il medesimo codice. I due thread condividono la variabile counter. Più avanti vedremo tecniche per la sincronizzazione dei processi. I task di linux Linux utilizza la stessa rappresentazione interna sia per i processi sia per i thread che in entrambi i casi vengono chiamati task. La differenza sta nel fatto che un thread è un task che condivide lo stesso spazio degli indirizzi con il genitore. Il processo ha uno spazio diverso. Questa differenza si nota nel momento della creazione dei task effettuando una system call opportuna:

  • Fork : crea un nuovo task con un suo nuovo contesto, cioè processo
  • Clone : crea un nuovo task con una sua identità propria, ma con i dati condivisi con il padre, cioè un thread. Le proprietà di un task linux possono essere classificate in tre categorie: identità del task, ambiente del task, contesto del task. 1. Identità del task
  • Process ID : identifica in modo univoco un task. Esempio: se un’applicazione deve effettuare una system call per inviare un messaggio, modificare o attendere un task, il parametro da passare al sistema operativo è il PID del task.
  • Credential : ad ogni task viene associato un user ID ed uno o più group ID. Serve per determinare i diritti di accesso del task a risorse e file.
  • Personality : questa caratteristica non si trova nei tradizionali sistemi Unix. Sotto linux ad

SINCRONIZZAZIONE unita 2

Distinguiamo due tipi di processi:

  1. Processi indipendenti : l’esecuzione di un processo non dipende dall’esecuzione degli altri processi, non c’è attesa tra processi.
  2. Processi cooperanti o concorrenti : l’esecuzione di un processo dipende dall’esecuzione degli altri processi. Il loro vantaggio sta nella condivisione delle informazioni, l’ accelerazione dell’elaborazione, la modularità e infine la convenienza. Interprocess Communication (IPC)  per permettere la cooperazione tra più processi o threads servono meccanismi per gestire la comunicazione e la sincronizzazione. Abbiamo due modelli: a memoria condivisa e a scambio di messaggi slide 6
  3. Modello a memoria condivisa: slide 7, se un processo modifica una variabile condivisa, anche gli altri processi vedranno la modifica. (la freccia rossa ci indica l’ordine delle istruzioni)
  4. Modello a scambio di messaggi: slide 8, i processi non condividono variabili in memoria, ma possono interagire scambiandosi messaggi. (la memoria è locale al processo P1 e P2 ha la sua memoria, lo scambio dei dati avviene nel momento dell’invio del msg, in quel momento parte la sincronizzazione, qualsiasi ordine vengono svolte le istruzioni non cambiano il risultato.

Corsa Critica

Si ha quando piu processi possono accedere e manipolare dei dati condivisi e il valore finale dei dati dipende dall’ordine degli accessi. Per prevenire la corsa critica i processi devono Sincronizzati tra loro. Osserva che quando si modifica deve essere l'unica che ha accesso all'area di memoria condivisa; quindi, l'unica soluzione è la sezione critica. La sezione critica sia quando n processi competono per accedere ad una data area condivisa. Ogni processo ha un segmento di codice, chiamato “Sezione critica”, Che contiene le istruzioni di accesso e manipolazione dei dati presenti nella memoria condivisa. La soluzione deve assicurare che un processo in esecuzione nella propria sezione critica, nessun altro processo possa essere in esecuzione nella propria sezione critica. La soluzione deve soddisfare tutte le proprietà elencate, altrimenti non funziona sempre:

  1. Mutua esclusione : se un processo P1 è in esecuzione nella propria sezione critica, nessun altro processo può essere in esecuzione nella propria sezione critica.
  2. Progresso : se nessun processo e nella sua sezione critica, ma ci sono altri processi che desiderano entrare nella propria sezione critica, allora la selezione di quale processo

può entrare nella sezione critica non può essere rimandata indefinitivamente. (assenza di deadlock).

  1. Attesa limitata: se esiste un processo A in attesa di entrare nella propria sezione critica, allora prima o poi entra in una sezione critica. (assenza di starvation). Assunzioni:
  2. Si deve assumere che ogni processo ha un tempo di esecuzione non nullo.
  3. Nessuna assunzione deve essere fatta circa la velocità relativa dei processi. La nostra soluzione astratta per la sezione critica è come segue: La prima cosa che deve fare, prima di entrare nella sezione critica, è eseguire o insieme di istruzioni per determinare si può accedere alla risorsa condivisa. è il permesso per entrare. Una volta ottenuto l'accesso, il thread esegue le istruzioni che manipolano la risorsa condivisa. Questo è il cuore dell'operazione. Ora, dopo aver completato il lavoro nella sezione critica, il thread Esegue altre istruzioni per dichiarare che non sta più utilizzando la risorsa condivisa, liberando così l'accesso per altri thread. Come vediamo nella slide 15, il nostro metodo run è modellato in modo tale che quando i due thread chiedono di accedere alla variabile contatore, leggera, incrementerà e uscirà cosicché anche l'altro threads entrerà e vedrà che contatore è uguale a uno e andrà a incrementare il contatore e avremo il contatore pari a due. Ora dobbiamo capire come realizzare queste due parti, entry e exit. Utilizziamo due approcci:
  4. Spinlock  si basa sull’attesa attiva. Il nostro processo va a controllare le nostre variabili di lock, se hanno un certo valore il nostro processo deve aspettare per poi ritornare a controllare più tardi finché non avrà l'accesso. Nome spinlock deriva proprio da queste variabili, il cui valore blocca o apre la sezione critica: spin perché il processo continuamente va a controllare queste variabili.
  5. Context switch  cambio di contesto processo va a vedere se ci sono le condizioni per entrare in sezione critica, se queste condizioni non ci sono il processo si sospende (chiede al sistema operativo di essere sospeso) quando queste condizioni cambiano sarà compito di chi cambia queste condizioni di andare ad avvertire il nostro processo. Nel primo caso la CPU più occupata dal nostro processo, nel secondo caso no.

SPINLOCK

Definiamo un'interfaccia dello spin lock alla slide 18 e andiamo a vedere delle possibili implementazioni di questa interfaccia.

Algoritmo tre (Peterson). Slide 22. I problemi visti fino ad ora portano a questo risultato. In questo algoritmo vediamo l’utilizzo delle flag e quindi dichiara se vuole entrare all’interno della sezione critica, però se vede un altro processo che vuole entrare, lascia il passo a quest’altro processo, immaginiamo come due persone che voglio entrare attraverso la stessa porta, uno dei due lascia il passo all’altro. quindi utilizza sia le flag che i turn. Processo P1 desidera entrare nella sua sezione critica, esegue i seguenti passaggi nella sezione di ingresso: dichiara l’intenzione di entrare, flag1 = true, mette la variabile turn a 2, dicendo che se l'altro processo P2 vuole entrare ha la precedenza. L'altro processo (P2) ha dichiarato la sua intenzione di entrare, flag2 = True, e il turno è del processo 2, turn = 2, quindi P1 resta in attesa, finché una delle due condizioni diventino false, ne basta una per far sì che P1 possa entrare:

  • se flag2 diventa False P2 non vuole entrare oppure è uscito, P1 puo entrare;
  • turn diventa 1 P2 ha ceduto il turno a P1, P1 può entrare. una volta che P1 è entrato e ha fatto quello che doveva fare, vuole uscire basta che cambia la variabile di flag, impostandola a False, il turn non lo tocca, non ha bisogno di toccarlo serve solo per risolvere i conflitti all’ingresso non all’uscita. Questo algoritmo soddisfa tutte e tre le proprietà andiamo a vedere cosa succede nei casi piu estremi.  foto Il problema dell’algoritmo di peterson è che non va piu bene se utilizzato con più di 2 threads; quindi, ora andiamo a vedere per quelli con n processi. Lamport Algoritmo del Fornaio di Lamport. Ad ogni processo viene assegnato un numero. Il processo con il numero più piccolo entra nella sua sezione critica. Quindi un processo che vuole entrare in sezione critica chiede chi è l’ultimo in coda di attesa e quando riceve una riposta si mette dietro l’ultimo. Chiedere chi è l’ultimo significa andare a vedere tutti i numeri e andare a prendere il massimo e incrementarlo di uno. Se più processi chiedono di entrare contemporaneamente di entrare il processo più anziano sarà quello che entrerà, per capire l’anzianità di un processo andiamo a vedere il valore del Process ID, più è piccolo più anziano sarà (è stato creato prima). Slide 24  andiamo a scrivere in java il nostro algoritmo definendo alcune costanti. abbiamo due array Choosing e Ticket.

Per entrambi nostri array, troviamo scritto volatile, Cosa significa? i compilatori moderni sono in grado di ottimizzare il codice per renderlo più veloce e più compatto, tra queste ottimizzazioni troviamo il riordinamento delle istruzioni: il compilatore può modificare l’ordine delle istruzioni nel codice macchia generato se ritiene che questo non alteri il risultato logico del programma dal punto di vista di un singolo thread. Specificare volatile su queste variabili inibisce queste ottimizzazioni rendendo il comportamento reale del programma il più vicino possibile a quello che il programmatore si aspetta e progetta. Nella slide 26 troviamo le operazioni da fare in ingresso, chiedere a tutti gli altri processi qual è il loro numero di ingresso e assegnarsi il massimo + 1, aspettando il proprio turno. terminato di lavorare in sezione critica, assegnare a sé stesso il ticket = 0, non ha bisogno di entrare. (per entrare il ticket parte da 1 in poi), il choosing mi dice in questo momento il thread si sta calcolando il suo ticket, ancora non lo sa, quando lo rimette a False nell’array abbiamo il valore del ticket aggiornato. nessuno dice ora è il tuo turno, e per sapere quando è arrivato il suo turno, il threads va a confrontarsi con tutti gli altri. Ciclo While siamo in attesa se “questo” o (||) “questo”. foto diagramma di sequenza sul telefono (5 foto = 5 lavagne). Il problema dell’algoritmo del fornaio è che usa θ(n) locazioni di memoria condivisa. Che è la quantita minima di memoria richiesta se si usano solo operazioni di lettura e scrittura. Stiamo parlando di una quantità di memoria che cresce con l’aumentare dei processi e questo lo rende poco pratico in sistemi con molti processi. Per queta ragione abbiamo bisogno di strumenti più potenti:

  1. Operazioni atomiche : istruzioni hardware che eseguono più passaggi (ad esempio leggere e modificare un valore) in un unico colpo.
  2. Operazioni di sospensione e riattivazione dei processi : meccanismi che permettono al sistema operativo di addormentare un processo in attesa e svegliarlo quando la risorsa è libera (es. semafori e monitor) evitando spreco di cpu. Operazioni Atomiche Analizzeremo due di queste operazioni fondamentali: Test-and-Set e Compare-and-Swap. L'operazione TestAndSet è un'istruzione atomica che:

 Se il lock era occupato (true): getAndSet restituisce true. La condizione while(true) è verificata, e il thread resta intrappolato nel ciclo, riprovando continuamente. Questo si chiama spin-lock. Seconda Soluzione (più potente): Compare-and-Swap (CAS) L'operazione CompareAndSwap (CAS) è un'altra istruzione atomica, ma più sofisticata della TSL. Funziona così:

  1. Confronta (Compare) il valore attuale di una variabile con un valore atteso (expectedValue).
  2. Solo se il valore attuale è uguale a quello atteso, lo aggiorna con un nuovo valore (newValue). Questa è l'operazione di Swap.
  3. Restituisce un'indicazione del successo dell'operazione. In Java, la classe AtomicBoolean fornisce il metodo compareAndSet(boolean expect, boolean update) che fa esattamente questo e restituisce true se lo scambio ha avuto successo, false altrimenti. Differenza chiave tra TSL e CAS:TSL (getAndSet) : Modifica il valore incondizionatamente.  CAS (compareAndSet) : Modifica il valore solo a una specifica condizione. Questa caratteristica la rende più flessibile e potente per algoritmi di sincronizzazione avanzati. ✅ Mutua Esclusione: Funzionano. Garantiscono che solo un thread alla volta entri nella sezione critica. ✅ Progresso: Se la sezione critica è libera, un thread che vuole entrare può farlo senza attendere indefinitamente. ✅ Attesa Limitata (Bounded Waiting): Questo è il loro principale difetto. Entrambe le implementazioni sono "ingiuste". Non c'è una coda. Se molti thread sono in attesa, competono tutti insieme ad ogni ciclo. Un thread sfortunato potrebbe continuare a perdere questa "gara" e non entrare mai nella sezione critica, soffrendo di starvation. Quindi queste due soluzioni hanno un grande svantaggio: costringono i processi in attesa a ciclare continuamente consumando CPU. Questo approccio è detto di busy waiting.

progresso. Ma non l’attesa limitata perché il fair che è impostato a false di default. Un thread appena arrivato può sorpassare quelli in coda. Se mettessi il fair = True non ho comunque la certezza che tutti i thread vengano trattati in modo equo, ciò dipende dalla gestione della JVM. Per quanto riguarda l’implementazione dell’interfaccia per i Semafori , java utilizza la classe Semaphore: quando è inizializzato a 1 abbiamo un semaforo binario (mutex) quando è inizializzaro un valore maggiore di 1 abbiamo un semaforo contatore. acquire() è il nostro wait, release() è il nostro signal, se il fair è impostato a True avremo tutte e tre le nostre proprieta garantite I semafori binari sono molto simili ai lock con una differenza: in un lock solo il thread che lo acquisice (decrementa il valore) lo può rilasciare (incrementa il valore), mentre nel semaforo binario un thread può decrementare il valore ed un altro lo può incrementare.

COUNTERSEMAPHORE

Implementazione di CounterSemaphore basata sul context switch, quindi non useremo l’attesa attiva (busy-waiting) ma utilizzeremo la capacità del sistema operativo di bloccare un thread e risvegliarlo quando serve, questo è un approccio più efficiente perche evita lo spreco di CPU. Utilizziamo due primitive per bloccare un processo e per risvegliare un processo:

  1. Wait() è la primitiva per bloccare, Quando un thread chiama wait() su un oggetto, viene sospeso (si "addormenta") e si mette in coda su quell'oggetto.
  2. notify(): Questa è una primitiva per risvegliare. Sceglie uno a caso tra i thread in attesa su quell'oggetto e lo risveglia (lo sposta nello stato "pronto" per essere eseguito). Il fatto che sia "non deterministico" (casuale) è un dettaglio cruciale.
  1. notifyAll(): Questa è un'altra primitiva per risvegliare , ma più potente. Risveglia tutti i thread che sono in attesa su quell'oggetto. e chiamate a wait(), notify() e notifyAll() devono obbligatoriamente avvenire all'interno di un blocco o metodo synchronized. Perché questi metodi operano sulla "serratura" (monitor) di un oggetto. Un thread deve prima possedere quella serratura per poter dire "ora mi metto in attesa su questa serratura" (wait) o "sveglio qualcuno in attesa su questa serratura" (notify). Il modo per ottenere la serratura in Java è, appunto, entrare in un blocco synchronized. Implementazione debole Viene creata una classe che implementa l'interfaccia Semaphore. Ha una variabile private int count; che rappresenta il numero di "permessi" disponibili del semaforo. publiv Syncronized void cSignal è il nostro rilasciare il permesso: La parola chiave synchronized è fondamentale: garantisce che solo un thread alla volta possa eseguire questo metodo (o il metodo cWait) sullo stesso oggetto, evitando che il count venga modificato in modo concorrente e corrotto. Count = count + Incrementa il numero di permessi disponibili. notify() risveglia un solo thread a caso tra quelli che sono in attesa nel metodo cWait. cWait sta per acquisire un permesso while (count == 0): Questa è la condizione di attesa. Il thread controlla se ci sono permessi

blocked.remove(): si rimuove dalla coda. count = count – 1: Finalmente, prende il permesso. if (!blocked.isEmpty()) notifyAll();: Se c'è almeno un thread in coda (!blocked.isEmpty()), sveglia tutti i thread in attesa.

Problemi Classici Di Sincronizzazione

Abbiamo:

  1. Barriere
  2. Produttore-consumatore
  3. Lettori e Scrittori
  4. Cinque Filosofi

BARRIERA

Una barriera è una forma di sincronizzazione dove esiste un punto (la barriera) nell’esecuzione di ogni processo di un certo gruppo che deve essere raggiunto da tutti i processi del gruppo prima che ognuno di loro possa proseguire nell’esecuzione. done[t0].release(): t0 ha finito il suo lavoro "prima della barriera". Con release(), alza la sua mano e dice "Io ci sono!". Il suo semaforo, done[0], passa da 0 a 1. done[t1].acquire(): Ora t0 si ferma e aspetta l'altro. Tenta di acquisire un permesso dal semaforo di t1. Si sta chiedendo: "È già arrivato t1?". Se t1 non è ancora arrivato, done[t1] è ancora a 0, e t0 si blocca. Immaginiamo che t0 arrivi per primo. Si bloccherà su done[t1].acquire(). Quando t1 arriva, esegue prima done[t1].release() (sbloccando potenzialmente t0) e poi done[t0].acquire(). Poiché t0 aveva già eseguito il suo release, done[t0] ha un permesso e t1 può procedere. Allo stesso tempo, t0 viene sbloccato perché t1 ha eseguito release sul suo semaforo.