Il 0% ha trovato utile questo documento (0 voti)
7 visualizzazioni10 pagine

C# 15.3 Thread Mutua Esclusione

Il documento descrive un'applicazione C# che utilizza thread per eseguire attività in parallelo, evidenziando il problema della mutua esclusione quando i thread condividono risorse. Viene presentata una soluzione tramite l'uso della parola chiave 'lock' per garantire l'accesso atomico alle risorse condivise, evitando condizioni di corsa. Infine, si discute l'importanza di progettare classi thread-safe per gestire correttamente le operazioni in un contesto multithreading.

Caricato da

alessandro121003
Copyright
© © All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Il 0% ha trovato utile questo documento (0 voti)
7 visualizzazioni10 pagine

C# 15.3 Thread Mutua Esclusione

Il documento descrive un'applicazione C# che utilizza thread per eseguire attività in parallelo, evidenziando il problema della mutua esclusione quando i thread condividono risorse. Viene presentata una soluzione tramite l'uso della parola chiave 'lock' per garantire l'accesso atomico alle risorse condivise, evitando condizioni di corsa. Infine, si discute l'importanza di progettare classi thread-safe per gestire correttamente le operazioni in un contesto multithreading.

Caricato da

alessandro121003
Copyright
© © All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Sei sulla pagina 1/ 10

MUTUA ESCLUSIONE 15.

3 C# Thread

15.3 C# Thread MUTUA ESCLUSIONE

Osserviamo la seguente applicazione C# che prevede:

• una classe Codice: per la rappresentazione del codice eseguibile indipendente da far
eseguire ai thread. Questa classe prevede il parametro nome del thread

• una classe Program per il Main thread che crea due thread th1 e th2.

using System;
using System.Threading;
namespace ThreadDueThread
{
class Codice
{
public void Attività(){
int d=100;
Console.WriteLine(Thread.CurrentThread.Name + " è stato creato");

while ( d > 0 ) //ciclo finito


{
Console.WriteLine("a " + Thread.CurrentThread.Name + " mancano " + d + "
cicli");
d--;
}
}

}
}

using System;
using System.Threading;
namespace ThreadDueThread
{
class Program
{
static void Main(string[] args)
{
Codice c1 = new Codice();
Codice c2 = new Codice();

alessandra peroni Pag. 1 di 10


MUTUA ESCLUSIONE 15.3 C# Thread

Thread th1 = new Thread(new ThreadStart(c2.Attività));


Thread th2= new Thread(new ThreadStart(c1.Attività));

th1.Name = “Primo”;
th2.Name = “Secondo”;

th1.Start();
th2.Start();

while(!th1.IsAlive && !th2.IsAlive); //il main thread aspetta che th1 e th2
siano effettivamente NATI (stato di Ready, Pronto)
Thread.Sleep(10); //il main thread si mette a dormire per un po' di tempo (10
millisec) per permettere a th1 e th2 di far qualcosa

th1.Join(); //attesa terminazione di th1


th2.Join();
Console.WriteLine("main thread è terminato così come th1 e th2");
}
}
}

Cosa fa questo programma?

Quando viene eseguito, il S.O. crea tre thread: il main thread, th1 e th2.

th1 e th2 eseguono lo stesso codice (quello definito dalla classe Codice). Questo codice
esegue un ciclo per 100 volte stampando a video una stringa ("a " +
Thread.CurrentThread.Name + " mancano " + d + " cicli"). Poi termina.

Il main thread prima di terminare aspetta che i due user thread th1 e th2 terminino.
Nel metodo main() infatti, troviamo le due istruzioni th1.Join(); e th2.Join();. Il
metodo Join() è una system call bloccante: il Main thread chiede al sistema
operativo di essere sbloccato (e quindi di continuare con l'istruzione successiva) solo quando il
thread specificato (th1 in th1.Join()) è terminato.

Lanciando più volte il programma, si nota che l'output ottenuto cambia.

Conclusione: l'ordine di esecuzione dei thread (e dei processi) non è garantito.

Sappiamo infatti che ogni thread viene schedulato secondo l'algoritmo di scheduling
implementato dal S.O. che, frequentemente, è un Round-Robin con classi di priorità.

alessandra peroni Pag. 2 di 10


MUTUA ESCLUSIONE 15.3 C# Thread

Questo algoritmo è di tipo preemptive: il S.O. interrompe un thread quando vuole


(perchè gli è scaduto il time slice, perchè è arrivata una richiesta di interruzione dal sistema, perchè... ne
ha voglia...).

Questo comportamento ha influenza quando i due thread, invece di essere


indipendenti come nell'esempio precedente, condividono una variabile.

Vediamo un esempio.

L'applicazione seguente è simile a quella precedente. La differenza è che il valore


decrementato è contenuto in un oggetto che i due thread condividono.

In questo caso abbiamo tre classi:

• la solita classe Codice

• la solita classe Program per il main thread

• una classe OggettoCondiviso per la definizione dell'oggetto che i due thread useranno

Il programma si prefigge lo scopo di far decrementare, dai due thread, il contenuto


dell'oggetto condiviso, mentre il suo valore è positivo. Quando il valore va a zero, i
thread si fermano.

Ecco il listato del programma:

using System;
namespace ThreadDueRaceCondition
{
class OggettoCondiviso
{
private int d;
public OggettoCondiviso(int d)
{
this.d = d;
}
public int Valore
{
get
{
return d;
}

alessandra peroni Pag. 3 di 10


MUTUA ESCLUSIONE 15.3 C# Thread

}
public int Dec()
{
--d;
return d;
}
}
}
using System;
using System.Threading;
namespace ThreadDueRaceCondition
{
class Codice
{
private OggettoCondiviso obj;

public Codice(OggettoCondiviso obj)


{
this.obj = obj;
}
public void Attività(){

Console.WriteLine(Thread.CurrentThread.Name + " è stato creato");


int valore;
do{
valore=obj.Valore;
if(valore > 0){
Console.WriteLine("mancano " + valore + " cicli");
obj.Dec();
}
}while(valore > 0);
Console.WriteLine(Thread.CurrentThread.Name + " termina");
}
}
}

alessandra peroni Pag. 4 di 10


MUTUA ESCLUSIONE 15.3 C# Thread

using System;
using System.Threading;
namespace ThreadDueRaceCondition
{
class Program
{
static void Main(string[] args)
{
OggettoCondiviso obj = new OggettoCondiviso(100);
Codice c1 = new Codice(obj);
Codice c2 = new Codice(obj);
Thread th1 = new Thread(new ThreadStart(c2.Attività));
Thread th2= new Thread(new ThreadStart(c1.Attività));
th1.Name = "Primo";
th2.Name = "Secondo";
th1.Start(); //il main thread chiede al S.O. di creare th
th2.Start();
th1.Name = "Primo";
th2.Name = "Secondo";
while(!th1.IsAlive && !th2.IsAlive);
Thread.Sleep(10);
th1.Join();
th2.Join();

Console.WriteLine("main thread è terminato così come th1 e th2");


}
}
}

Se ora lanciamo più volte l'esecuzione della nostra applicazione, possiamo vederne il
comportamento anomalo: siamo in presenza di corsa critica. Infatti il sistema non
garantisce che i due thread accedano all'oggetto condiviso per tutto il tempo
necessario alla lettura del valore e alla sua modifica. L'operazione svolta non è cioè
atomica.

Dobbiamo risolvere il problema!

alessandra peroni Pag. 5 di 10


MUTUA ESCLUSIONE 15.3 C# Thread

LOCK

Come sappiamo dalla teoria dei S.O., la soluzione consiste nel rendere atomico
l'accesso alla risorsa condivisa o, in altre parole, garantire l'accesso alla risorsa
condivisa in mutua esclusione.

Il sistema operativo fornisce system call specifiche, che i vari linguaggi di


programmazione girano ai programmatori.

Il C#, a questo scopo, fornisce la parola chiave lock che consente di definire un blocco
di codice come regione critica (zona di codice dove si fa uso dell'oggetto condiviso) e assicura
l'accesso in mutua esclusione (un thread non può entrare nella regione critica se un altro thread
già la occupa).

Se un thread cerca di entrare in una regione critica bloccata (locked), il sistema


operativo lo blocca (il thread viene posto in stato di blocked, waiting).

ATTENZIONE: se un thread non riesce ad accedere alla regione critica perchè un altro
thread non ne esce, il thread resta bloccato all'infinito (deadlock).

Il comando lock ha questo funzionamento:

1. ottiene il lock per un certo oggetto

2. permette attività sull'oggetto

3. rilascia il lock

La sintassi di lock è la seguente:

lock (obj) obj deve essere una variabile riferimento. Spesso si ha lock(this)
{
istruzioni;
}

Se si vuole proteggere con lock una variabile statica o se la regione critica si trova in
un metodo statico, non si può ovviamente eseguire il lock di una variabile riferimento.
Si deve procedere invece in questo modo:

class Scatola
{
public static void Add(object x) {
lock (typeof(Scatola)) {

alessandra peroni Pag. 6 di 10


MUTUA ESCLUSIONE 15.3 C# Thread

...
}
}
public static void Remove(object x) {
lock (typeof(Scatola)) {
...
}
}
}

Modifichiamo la classe Codice così da proteggere le regioni critiche. Ecco la nuova


versione:

namespace ThreadDueLock
{
class Codice
{
private OggettoCondiviso obj;

public Codice(OggettoCondiviso obj)


{
this.obj = obj;
}
public void Attività(){
Console.WriteLine(Thread.CurrentThread.Name + " è stato creato");
int valore;
do{
lock(obj){
valore=obj.Valore;
if(valore > 0){
Console.WriteLine("mancano " + valore + " cicli");
obj.Dec();
}
}
}while(valore > 0);
Console.WriteLine(Thread.CurrentThread.Name + " termina");
}
}
}

alessandra peroni Pag. 7 di 10


MUTUA ESCLUSIONE 15.3 C# Thread

Il resto del programma resta inalterato.


Provando a eseguire più volte l'applicazione, possiamo notare che il valore dell'oggetto
condiviso è esattamente come ce lo aspettiamo: viene decrementato in modo corretto.
Quale thread poi lo decrementa, cambia da volta a volta.

Come al solito, non è dato sapere l'ordine di esecuzione dei thread.

CONSIDERAZIONI SULLA PROGETTAZIONE DI OGGETTI

Ci si può porre il problema se la soluzione, sopra offerta per l'accesso in mutua


esclusione all'oggetto condiviso, sia o meno corretta.

Non sono i metodi di OggettoCondiviso a dover essere protetti?

Probabilmente sì.

E' dunque opportuno, quando è possibile farlo, progettare classi thread safe, cioè
classi che si comportano in modo corretto in caso di applicazioni multithreading.
Riscriviamo pertanto il programma precedente, così che sia la classe OggettoCondiviso
ad essere thread safe e a fornire, quindi, tutti gli accessi ai dati (nel nostro caso d) in
mutua esclusione.

using System;
using System.Thread5ng;
namespace ThreadDueLock
{
class OggettoCondiviso
{
private int d;
public OggettoCondiviso(int d)
{
this.d = d;
}
public int Valore
{
get
{
lock (this)
{
return d;
}

alessandra peroni Pag. 8 di 10


MUTUA ESCLUSIONE 15.3 C# Thread

}
}
public int Dec()
{
lock (this)
{
--d;
return d;
}
}
public void DecEndShow() {
lock(this){
Dec();
Console.WriteLine( Thread.CurrentThread.Name + " stampa valore " +
Valore);
}
}
}
}

using System.Threading;
namespace ThreadDueLock
{
class Codice
{
private OggettoCondiviso obj;
public Codice(OggettoCondiviso obj)
{
this.obj = obj;
}
public void Attività(){
Console.WriteLine(Thread.CurrentThread.Name + " è stato creato");

for (int i = 0; i < 100;i++)


{
obj.DecEndShow();
}
Console.WriteLine(Thread.CurrentThread.Name + " termina");
}

alessandra peroni Pag. 9 di 10


MUTUA ESCLUSIONE 15.3 C# Thread

}
}

La classe Program resta inalterata.

La soluzione di accesso atomico, fornita negli esempi precedenti, è quella da utilizzare


in problemi di tipo Lettore-Scrittore, in cui il thread Lettore e il thread Scrittore
svolgono attività asincrone tra loro, e quindi non dipendenti dall'ordine di
accesso alla risorsa condivisa (quindi non dipendenti dall'ordine di esecuzione dei
thread). Il Lettore può infatti leggere quante volte vuole e quando vuole il dato
condiviso. Altrettanto può fare lo Scrittore. L'importante è che l'attività di modifica
dello Scrittore sia fatta assolutamente in mutua esclusione.

ESERCIZIO:

Scrivere un'applicazione che simuli l'attività tipica del Lettore-Scrittore.

alessandra peroni Pag. 10 di 10

Potrebbero piacerti anche