Professional Documents
Culture Documents
Multithreading in .NET e C#
Multithreading in .NET e C#
NET E C#
I concetti di programmazione concorrente esistono anche nel mondo .NET. Nel namespa-
ce System.Threading si trovano tutta una serie di classi fondamentali per la realizzazione
di programmi concorrenti. Con Thread è possibile modellare thread in esecuzione, con
Monitor è possibile modellare mutex, con Event è possibile la modellizzazione di thread
pools, e così via.
Threads
Di base, un programma in .NET parte con un thread principale. Se il nostro programma è
a riga di comando, questo è ovvio. Se invece è con interfaccia grafica, c’è comunque un
main() che esegue un metodo chiamato Application.Run, responsabile della creazione
di un’istanza della classe principale e dell’esecuzione di un ciclo (il loop dei messaggi) in
cui si attendono eventi dal mondo esterno.
Il costruttore Thread() vuole un parametro delegate di una funzione che non ha para-
metri e ritorna void, che è il “metodo” che dev’essere eseguito dal thread. Come in Java,
il thread (o meglio, l’oggetto gestito) è creato ma non è fatto partire.
La schedulazione parte nel momento in cui si invoca Start(), che comporta l’effettiva
creazione del thread nativo e la successiva esecuzione. Questo thread si comporta come
tutti i thread e si fa i fatti suoi. Andrà in esecuzione fino a quando il metodo passato
come parametro al thread non ritorna. Possiamo aspettare esplicitamente che il thread
termini l’esecuzione, invocando Join(). C’è una versione overloaded che permette di
aspettare per un certo timeout.
L’ultimo metodo degno di nota (e fonte di guai) è Abort(). E’ una forzatura per far smet-
tere un thread, grazie all’intervento della VM che lancia un’eccezione di tipo ThreadA-
bortException che è catchabile, ma è rilanciata fino in cima (e quindi tanto vale non far-
lo).
public sealed class Thread {
/* Proprietà */
public IsAlive{get;}
public IsBackground{get;set;}
/* Membri statici */
public static Thread CurrentThread{get;}
public static void Sleep(int ms);
}
Come abbiamo già detto, il costruttore del thread vuole un parametro di tipo delegate
che non ha parametri e ha void come tipo di ritorno. Normalmente si può passare un ri-
ferimento di un metodo specifico associato a un’istanza particolare, ma è anche lecito
passare un metodo statico.
class foo {
private int a;
private int b;
private void ThreadFcn() { /* Mestiere da fare: stampo fino a this.a */
int i;
for (i = 0 ; i < this.a ; i++)
Console.WriteLine("Number {0}", i);
}
class foo {
private int a;
private int b;
private void ThreadFcnA() {
int i;
for ( i = 0 ; i < this.a ; i++ )
Console.WriteLine("Number {0}", i);
}
private void ThreadFcnB() {
int i;
for (i = 0 ; i < this.b ; i++)
Console.WriteLine("Number {1}", i);
}
public void ExecuteMyThread(int n) {
a = n;
b = n;
ThreadStart MyDelegate = new ThreadStart(this.ThreadFcnA);
MyDelegate += new ThreadStart(this.ThreadFcnB);
Thread MyThread = new Thread(MyDelegate);
MyThread.Start();
MyThread.Join();
}
}
Occhio però! Viene creato un unico thread che prima chiama tutta la funzione A e poi
tutta la funzione B. Quindi sebbene un codice del genere è potenzialmente scrivibile, è
meglio non farlo. Se una di queste funzioni poi lancia un’eccezione stiamo proprio una
crema, visto che tutte le altre non vengono nemmeno eseguite.
Terminazione di un thread
La terminazione di un thread è fonte di problemi. E’ molto più complicato terminare cor-
rettamente i thread che non crearli, perché dobbiamo fermare un processo in corso. Un
thread deve dunque essere progettato sin dall’inizio sapendo che è cancellabile.
I signori di .NET dunque hanno provato a inserire Abort() per evitare di spaccarsi la te-
sta. Se da qualche parte conosciamo l’identità di un System.Threading.Thread e su que-
sto invochiamo Abort(), il corrispondente thread gestito viene flaggato e quando verrà
eseguita la prossima istruzione verrà lanciata un’eccezione ThreadAbortException, che
come già detto è catchabile ma non ignorabile perché viene rilanciata automaticamente
dal runtime all’uscita del blocco finally (se esiste).
C’è un modo per bloccare la propagazione, col metodo ResetAbort(). Se però qualcuno
ha chiesto di abortire aveva le sue ottime ragioni, e quindi il rischio macello è elevatissi-
mo. Quindi non serviamoci dei meravigliosi strumenti offerti dai signori di .NET, per di-
versi motivi:
• un’eccezione è un evento eccezionale, la terminazione di un thread no
• usare Abort() fa terminare il codice in maniera brutale
• già fare il catch quando mi aspetto un’eccezione è delicato, così diventa un delirio
Per fare le cose pulite possiamo usare uno schema di questo genere
class foo {
public volatile bool TerminateThread = false; /* volatile così leggo sempre memoria */
public void ThreadFcn() {
/* ... */
while (!this.TerminateThread /* && other == something */) {
/* Fai qualcosa */
}
/* Cose da fare prima di salutare tutti */
}
}
/* Altro thread */
fooObject.TerminateThread = true;
MyThread.Join();
C’è anche un altro modo per lavorare, sfruttando degli oggetti di Win32. Di base, Win32
offre come oggetti kernel usabili per comunicare tra processi diversi (ma volendo uguali)
delle cose che non hanno nessun paragone nel mondo di Linux. Tra questi ci sono gli og-
getti Event, tra cui si possono distinguere gli AutoResetEvent e i ManualResetEvent.
Gli Event in generale sono una specie di flag: posso aspettare un evento e bloccarmi fin-
ché qualcuno non lo segnala. La differenza tra AutoResetEvent e ManualResetEvent è che
entrambi sono bloccanti, ma i secondi vengono accesi e spenti esplicitamente, mentre i
primi sono spenti quando qualcuno li aspetta e poi li accoglie.
class foo {
public AutoResetEvent TerminateEvent = new AutoResetEvent();
public void ThreadFcn() {
WaitHandle []Handles = new WaitHandle[2];
Handles[0] = TerminateEvent;
/* Handles[1] = some other event related to IO */
Entrambe le soluzioni hanno vantaggi e svantaggi: nel primo caso riesco ad aumentare
la riusabilità del mio codice da contesti di elaborazione differenti, ma perdo il senso del
fatto che una certa classe è usata in un contesto parallelo in un determinato scopo. Nel
secondo caso riesco dunque ad aumentare in chiarezza a discapito della riusabilità.
Non bisogna poi, come già detto, registrare più di un metodo sul delegato ThreadStart.
Se un thread lancia un eccezione in C++ e questa non è catchata, la cosa porta alla ter-
minazione di tutto il processo. C# invece non fa arrivare l’eccezione fino alla runtime li-
brary in modo da non far abortire il processo, il che non è sempre una grandissima idea.
Questo perché la sincronizzazione comporta una perdita di prestazioni, che non piace
perché sull’interfaccia vogliamo un’alta reattività.
Sincronizzazione
La sincronizzazione viene fatta con l’equivalente degli stessi metodi che abbiamo visto
in C++ (mutex per non toccare le stesse variabili, condition_variable per avvisare che è
successo qualcosa).
Win32 .NET
Mutex Mutex
CriticalSection Monitor (il corrispondente del mutex C++)
Event AutoResetEvent
ManualResetEvent
Semaphore Semaphore
La classe Monitor
Rappresenta l’unione dei concetti mutex e condition_variable in C++11. Permette di
definire una regione critica di codice (“solo un thread alla volta all’interno dello stesso
processo”).
Di seguito due esempi d’uso. Il primo è un po’ pericoloso, perché nel mezzo ci potrebbe-
ro essere delle eccezioni. Quindi meglio una cosa fatta come nel secondo esempio, in
cui si fa una specie di equivalente di quello che si faceva con unique_lock per usare il
paradigma RAII.
In realtà una roba come quella di destra è modellabile col costrutto C# lock(), che com-
pila un codice come quello riportato qui sotto con try e finally.
class foo {
private int a = 0;
private int b = 0;
Le Handle rappresentano oggetti kernel Win32 che possono avere due stati, segnalato e
non segnalato. Dato che internamente chiamano WaitForSingleObject e compagnia bel-
la, si passa in kernel mode (e quindi i tempi non sono da sottovalutare). Se dunque non
devo fare sincronizzazione tra processi separati, è meglio usare i Monitor.
I Mutex si utilizzano per creare sezioni critiche di codice (“un thread alla volta”). L’aspet-
to interessante è che è aggiunto il metodo ReleaseMutex() che ci permette di rilasciarlo.
Vanno usati in un contesto di tipo try/finally, in modo da essere sicuri di rilasciarli se li
prendiamo.
Sono ricorsivi (quindi posso fare più volte WaitOne e per rilasciarlo devo fare altrettante
volte ReleaseMutex). Non si può rilasciare un Mutex acquisito da un altro thread, pena la
generazione di una ApplicationException. Se un thread possiede un Mutex e questo ter-
mina, il Mutex è rilasciato automaticamente dal runtime (cosa che non succede negli og-
getti di tipo Monitor)
class foo {
private int a = 0;
private int b = 0;
private Mutex MyMutex = new Mutex();
Mutex e Monitor fanno un mestiere che concettualmente è molto simile, perché garanti-
scono che un thread solo alla volta ne può prendere il possesso. Un Monitor però usa
l’indirizzo di un oggetto come token di sincronizzazione, mentre il Mutex è un oggetto
kernel a sé stante e quindi è condivisibile tra processi diversi.
Gli Event sono oggetti kernel Win32 usabili per fare le veci delle condition_variable, ma
hanno una semantica differente in quanto è possibile fare la WaitOne() senza possedere
nessuna risorsa.
***