UniTO/anno2/YearI/FirstSem/MCAD/lesson4-10102017.md
Francesco Mecca 5e286062f8 MCAD 2019
2018-11-22 13:09:11 +01:00

278 lines
7.5 KiB
Markdown

# Generalizzazione al problema della Mutua Esclusione
Esempio: strada a senso unico alternato. Piu' mezzi possono passare nella stessa direzione, ma non due mezzi in senso inverso.
## Soluzione al problema: mutex
Abbiamo un numero N di operazioni su sezione critica OP1, OP2, ... OPi, OPN con N>1.
Utilizziamo un semaforo mutex e un semaforo per ogni OPi (Mi), assieme a un contatore count_i:
semaphore mutex = 1
semaphore Mi = 1 <- Mi e' utilizzato per proteggere count_i
int count_i = 0
```
public void OPi() {
P(Mi);
count_i++;
if (count == 1) { P(mutex); } // il primo processo che ha cercato di entrare con la risorsa occupata e' fermo su mutex
V(Mi);
.
.
OPi
.
.
P(Mi);
count_i--;
if (count_i == 0) { V(mutex); }
V(Mi);
}
```
Esempio:
```
public void da_nord() {
P(Mnord);
count_nord++;
if(count_nord == 1) P(mutex);
V(Mnord);
.
.
Transito da nord
.
.
P(Mnord);
count_nord--;
if (count_nord == 0) V(mutex);
V(Mnord);
}
```
Questo e' un esempio del problema di **Writers & Readers** (Lettori e scrittori), dove su un file condiviso i lettori possono leggere in contemporanea ma gli scrittori devono lavorare uno alla volta.
## Readers & Writers (lettori e scrittori)
* Readers:
semaphore mutex = 1
semaphore legge = 1
int count = 0
```
public void lettura () {
P(legge);
count++;
if (count == 1) P(mutex);
V(legge);
.
leggi
.
P(leggi)
count--;
if (count == 0) V(mutex);
V(leggi);
}
```
* Writers:
```
public void scrivi () {
P(mutex);
.
scrivi
.
V(mutex);
}
```
Questa soluzione e' soggetta a **starvation** da parte degli scrittori, perche' se continuano ad arrivare lettori ininterrottamente, non c'e' garanzia che i writers riescano ad ottenere la risorsa.
### Dimostrazione della correttezza - r&w
* Se non ci fossero lettori, varrebbe l'invariante semaforico per mutex:
```
nP(mutex, t) - nV(mutex, t) = nWriters (se nReaders = 0);
```
* Se ci sono lettori:
```
nP(mutex, t) - nV(mutex, t) = nWriters + 1 (se nReaders != 0);
```
* Posso quindi affermare:
```
0 <= nP(mutex, t) - nV(mutex, t) <= 1
```
* Se non ci sono lettori, ottengo:
```
0 <= nWriters <= 1 (Che e' corretto, in quanto con un semaforo binario puo' esserci al piu' uno scrittore)
```
* Se ci sono lettori:
```
nWriters + 1 <= 1 -> nWriters = 0 (corretto, non possono esserci lettori e scrittori contemporaneamente)
```
* La soluzione e' quindi corretta.
## Semafori per sincronizzazione tra processi
Supponiamo due processi:
* P1, che deve effettuare operazione A
* P2, che deve effettuare operazione B
E pongo che A dev'essere eseguita **dopo** B.
Un modo e' introdurre un semaforo:
semaphore sync = 0;
```
P1 P2
. .
. .
P(sync) B
A V(sync)
. .
. .
```
#### Dimostrazione di correttezza
Supponiamo che A venga eseguito *prima* di B. (**Per Assurdo**).
Avro' quindi:
```
(sequenza temporale)
--P(sync)--A-----B--V(sync)--
```
* Che pero' vorrebbe dire:
```
nP (sync, t) = 1
nV (sync, t) = 0
nP (sync, t) <= nV (sync, t) <- NON E' POSSIBILE
```
* dimostrato
### Problema del rendez-vous
Due processi che si devono aspettare per effettuare una determinata operazione.
semaphore sem1 = 0;
semaphore sem2 = 0;
```
P1 P2
. .
. .
A1 B1
V(sem1) V(sem2)
P(sem2) P(sem1)
A2 B2
. .
```
Qui, i due processi segnalano al concorrente che hanno effettuato l'operazione A1/B1, e quindi proseguono con A2/B2.
## Produttore e Consumatore
### 1 Produttore, 1 consumatore (caso semplice = buffer unitario)
Vogliamo che il produttore riempia un buffer solo quando il consumatore l'ha consumato.
Il consumatore non deve prelevare due volte lo stesso dato.
Due semafori:
semaphore empty = 1 // notifica che il buffer e' vuoto (vuoto a inizializzazione)
semaphore full = 0 // notifica che il buffer e' pieno
* Inserimento (producer)
```
void invio (dato T) {
P(empty);
...inserisco dato...
V(full);
}
```
* Estrazione (consumer)
```
T estrai () {
T dato;
P(full);
...
dato = datoestratto;
...
V(empty);
return dato;
}
```
Nota: le operazioni di estrazione e inserimento non sono atomiche, e' necessario dimostrare che non serve utilizzare un mutex per proteggere l'accesso ed evitare la mutua esclusione.
#### Dimostrazione di correttezza (p&c)
nd(t) = numero totale di operazioni di deposito (produttore inserisce)
ne(t) = numero totale di estrazioni (consumatore estrae)
* Quindi, io posso al massimo fare un deposito in piu' di un'estrazione:
```
nd(t) <= ne(t) + 1
ne(t) <= nd(t) // non posso estrarre se non ho depositato
```
* Posso riscrivere le due operazioni come:
```
0 <= nd(t) - ne(t) <= 1 // e' quello che voglio dimostrare
```
* Nota: queste procedure (estrazione, deposito) possono essere chiamate piu' volte in sequenza. Posso pero' affermare che il deposito e' effettuato solo dal produttore, che utilizza la procedura sopra illustrata. Quindi:
```
nV(full,t) <= nd(t) <= nP(empty,t)
nV(empty,t) <= ne(t) <= nP(full,t) // le due disuguaglianze sono simmetriche
```
* A questo posso aggiungere l'invariante semaforico:
```
ne(t) <= nP(full,t) <= nV(full,t) <= nd(t) <= nP(empty) <= nV(empty,t) + 1 <= ne(t) + 1
```
* Ottengo:
```
ne(t) <= nd(t) <= ne(t)+1
```
* Da cui:
```
0 <= nd(t) - ne(t) <= 1
```
* Che quindi dimostra la correttezza della procedura
#### Producer & Consumer - Mutua esclusione (buffer unitario)
**Per Assurdo:**
Esiste un'istante T in cui il consumatore estrae e il produttore deposita (il che violerebbe il principio di mutua esclusione).
* Dal codice:
```
nP(empty, T) = 1 + nV(full, T) -> nV(full, T) = nP(empty, T) - 1
nP(full, T) = 1 + nV(empty, T)
```
* Unendo le due con l'invariante semaforico:
```
nP(empty, T) <= nV(empty, T) + 1 <= nP(full, T) <= nV(full, T) <= nP(empty, T)
```
* Ho quindi dimostrato che:
```
nP(empty, T) <= nP(empty,t) - 1 // IMPOSSIBILE
```
* per assurdo, dimostrato
Questo dimostra che non e' necessario proteggere il buffer con un mutex dato che la soluzione non viola **mai** il processo di mutua esclusione, ma solo nel caso in cui il buffer sia unitario. Il caso di buffer circolare non e' considerato, potrebbe essere necessario un mutex.
## Semafori e Sezioni critiche condizionali
Spesso, se una struttura condivisa deve essere utilizzata da piu' processi, si crea una condizione da controllare che e' unica alla sezione critica. Questo vuol dire che un processo deve controllare la condizione accedendo alla sezione critica, ma questo potrebbe non essere possibile (nel caso questa fosse bloccata da un altro processo).
Consideriamo una regione R:
La procedura implica entrare nella sezione critica, verificare C e se vera eseguire S, poi liberare la sezione critica.
Se C non e' verificata, il processo deve invece uscire e aspettare.
```
region R << when(C) S; >>
```
Abbiamo quindi la situazione detta di **attesa attiva**:
1. Processo entra nella sezione critica
2. Testa la condizione: se e' vera, esegue la procedura ed esce
3. Se la condizione e' falsa, deve uscire e tornare a 1
### Soluzione mutex
Quando il processo trova una condizione falsa nella sezione critica, dovrebbe tornare all'inizio, rientrare e testare la condizione. Nel peggiore dei casi, tutte le volte che un processo esegue un'operazione S con successo, cambia i valori della condizione C. In questo modo posso utilizzare un semaforo per bloccare il ritorno al test.
1. Processo entra nella sezione critica
2. Testa la condizione: se e' vera, esegue la procedura ed esce
3. Se la condizione e' falsa, deve uscire e aspettare V(sem) per tornare a 1
In questo modo l'attesa non e' piu' attiva, e il processo ha piu' possibilita' di entrare nella sezione critica.