C# 15.3 Thread Mutua Esclusione
C# 15.3 Thread Mutua Esclusione
3 C# Thread
• 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");
}
}
using System;
using System.Threading;
namespace ThreadDueThread
{
class Program
{
static void Main(string[] args)
{
Codice c1 = new Codice();
Codice c2 = new Codice();
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
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.
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à.
Vediamo un esempio.
• una classe OggettoCondiviso per la definizione dell'oggetto che i due thread useranno
using System;
namespace ThreadDueRaceCondition
{
class OggettoCondiviso
{
private int d;
public OggettoCondiviso(int d)
{
this.d = d;
}
public int Valore
{
get
{
return d;
}
}
public int Dec()
{
--d;
return d;
}
}
}
using System;
using System.Threading;
namespace ThreadDueRaceCondition
{
class Codice
{
private OggettoCondiviso obj;
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();
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.
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 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).
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).
3. rilascia il lock
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)) {
...
}
}
public static void Remove(object x) {
lock (typeof(Scatola)) {
...
}
}
}
namespace ThreadDueLock
{
class Codice
{
private OggettoCondiviso obj;
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;
}
}
}
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");
}
}
ESERCIZIO: