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 [Link];
namespace ThreadDueThread
{
class Codice
{
public void Attività(){
int d=100;
[Link]([Link] + " è stato creato");
while ( d > 0 ) //ciclo finito
{
[Link]("a " + [Link] + " mancano " + d + "
cicli");
d--;
}
}
}
}
using System;
using [Link];
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([Link]à));
Thread th2= new Thread(new ThreadStart([Link]à));
[Link] = “Primo”;
[Link] = “Secondo”;
[Link]();
[Link]();
while(![Link] && ![Link]); //il main thread aspetta che th1 e th2
siano effettivamente NATI (stato di Ready, Pronto)
[Link](10); //il main thread si mette a dormire per un po' di tempo (10
millisec) per permettere a th1 e th2 di far qualcosa
[Link](); //attesa terminazione di th1
[Link]();
[Link]("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 " +
[Link] + " 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 [Link](); e [Link]();. 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 [Link]()) è 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 [Link];
namespace ThreadDueRaceCondition
{
class Codice
{
private OggettoCondiviso obj;
public Codice(OggettoCondiviso obj)
{
[Link] = obj;
}
public void Attività(){
[Link]([Link] + " è stato creato");
int valore;
do{
valore=[Link];
if(valore > 0){
[Link]("mancano " + valore + " cicli");
[Link]();
}
}while(valore > 0);
[Link]([Link] + " termina");
}
}
}
alessandra peroni Pag. 4 di 10
MUTUA ESCLUSIONE 15.3 C# Thread
using System;
using [Link];
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([Link]à));
Thread th2= new Thread(new ThreadStart([Link]à));
[Link] = "Primo";
[Link] = "Secondo";
[Link](); //il main thread chiede al S.O. di creare th
[Link]();
[Link] = "Primo";
[Link] = "Secondo";
while(![Link] && ![Link]);
[Link](10);
[Link]();
[Link]();
[Link]("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)
{
[Link] = obj;
}
public void Attività(){
[Link]([Link] + " è stato creato");
int valore;
do{
lock(obj){
valore=[Link];
if(valore > 0){
[Link]("mancano " + valore + " cicli");
[Link]();
}
}
}while(valore > 0);
[Link]([Link] + " 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();
[Link]( [Link] + " stampa valore " +
Valore);
}
}
}
}
using [Link];
namespace ThreadDueLock
{
class Codice
{
private OggettoCondiviso obj;
public Codice(OggettoCondiviso obj)
{
[Link] = obj;
}
public void Attività(){
[Link]([Link] + " è stato creato");
for (int i = 0; i < 100;i++)
{
[Link]();
}
[Link]([Link] + " 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