You are on page 1of 8

M21. MULTITHREADING IN .

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.

Se vogliamo creare un programma concorrente lo facciamo istanziando la classe Thread.


Attenzione però: in .NET a ogni thread costruito (gestito) corrisponde sempre un thread
nativo del sistema operativo. Non è vero il contrario: all’interno del nostro programma
.NET, oltre ai thread che abbiamo creato noi, possono esistere dei thread nativi che non
hanno corrispondenza nel mondo .NET. Un classico esempio è il thread che fa garbage
collection. Questa sottolineatura è importante perché è possibile caricare delle DLL
da .NET. La DLL potrebbe creare dei thread, che non sottostanno alle regole di .NET.

Così come in Java, .NET permette la creazione di due tipi di thread:


• foreground thread: thread che “tengono in vita” l’applicazione
◦ fintanto che c’è un thread foreground in esecuzione, il main non può uscire
• background thread: thread che partono e vanno
◦ se finisce il thread principale, il runtime forza la chiusura di questi thread

Membri della classe Thread


public sealed class Thread {
public Thread(ThreadStart start); /* Costruttore */
public void Start(); /* Creatore e attivatore */
public void Join(); /* Mi blocco e aspetto terminazione */
public void Abort(); /* Termina un thread */
...
}

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);
}

Tra le proprietà possiamo citare


• IsAlive, che restituisce true se il thread è stato lanciato ma non ha ancora termi-
nato la sua computazione
◦ non è usabile per la sincronizzazione
◦ è evanescente
• IsBackground, per impostare un thread come thread di background o vedere se
quel dato thread è un thread di background

Tra i membri statici invece abbiamo


• CurrentThread, che ci permette di avere un riferimento al thread
• Sleep(int ms), che permette di mandare in pausa per un certo numero di secondi
il thread corrente senza consumare CPU
◦ non è detto che poi riprenda subito
◦ se ms = 0 è l’equivalente di yield()

Costruttore della classe Thread


public Thread(ThreadStart start);
public delegate void ThreadStart();

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);
}

public void ExecuteMyThread(int n) {


a = n;
ThreadStart MyDelegate = new ThreadStart(this.ThreadFcn); /* Istanzia delegato */
Thread MyThread = new Thread(MyDelegate); /* Crea il thread */
MyThread.Start(); /* Parte l'elaborazione */
...
MyThread.Join(); /* Attendo */
}
}
Attenzione: i delegati sono intrinsecamente multicast, quindi posso registrare più cose

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 */

while (WaitHandle.WaitAny(Handles) == 1 /* && other */) {


/* il thread non deve terminare e fa qualcosa */
}
/* Cose da fare prima di salutare tutti */
}
/* Altro thread */
fooObject.TerminateEvent.Set();
MyThread.Join();

Considerazioni sui thread


Quando progettiamo un programma concorrente sorgono una serie di considerazioni.
Una di queste si domanda se creare il metodo da invocare nel thread dentro una classe
tutta sua o come metodo di una classe più ampia che conosce l’identità del thread.

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.

Thread e applicazioni grafiche


Quando creiamo un programma con interfaccia grafica, l’accesso a tutto ciò che è grafi-
co (operazioni che disegnano sullo schermo, interazioni coi bottoni...) può solo essere
fatta dal thread che ha creato la finestra dentro cui l’interfaccia è ospitata. In .NET dun-
que lo può fare solo il thread principale.

Questo perché la sincronizzazione comporta una perdita di prestazioni, che non piace
perché sull’interfaccia vogliamo un’alta reattività.

Se ci sono operazioni lunghe da fare (interrogazioni su un database, scaricamento della


posta...) è meglio che queste siano fatte con thread secondari al fine di non rendere
l’interfaccia “scattosa”. Il problema è che il thread secondario fa l’operazione, ma non
può andare sugli oggetti risultati per “mettere a posto il risultato”: occorre dunque met-
tere in atto una comunicazione bidirezionale in cui il thread principale crea un thread se-
condario che fa le azioni e il thread secondario, quando ha finito, mette in atto una co-
municazione all’indietro dicendo “ho finito e questo è il risultato”.

Il primo passo è relativamente facile, il secondo è leggermente diverso tra Windows


Form e WPF. Quando usiamo Windows Form tutto ciò che è grafico deriva da una classe
comune che si chiama System.Windows.Forms.Control: questo ci viene utile perché Con-
trol ha dentro di sé tre metodi che fanno quello che ci interessa.

public class Control {


/* Se tu sei un thread secondario e mi conosci, puoi chiamare Invoke */
/* Mi passi un delegato il cui metodo deve tornare void ma può avere N argomenti */
/* Gli argomenti me li passi in un array separato */
/* La chiamata è bloccante */
public object Invoke(Delegate method, object []args);

/* Invocazione in maniera asincrona */


/* A differenza di Invoke non mi fa aspettare, ma restituisce un token */
/* Meccanismo simile ad async/future */
public IAsyncResult BeginInvoke(Delegate method, object []args);
/* Attende la terminazione */
public object EndInvoke(IAsyncResult async);
}

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”).

public sealed class Monitor {


private Monitor(); /* Non istanziabile, ma non mi serve perché i metodi sono static */

/* Equivalente di mutex.lock e mutex.unlock. */


/* obj è l’indirizzo dell’oggetto su cui voglio sincronizzarmi */
/* Un solo thread alla volta può entrare in una sezione critica identificata da
un certo oggetto obj, che deve essere un reference type */
public static void Enter(object obj); /* Entrata nella regione critica */
public static void Exit(object obj); /* Uscita dalla regione critica */

/* Equivalenti a metodi offerti da condition_variable. */


/* obj è la condition_variable associata al monitor. */
public static void Wait(object obj);
public static void Pulse(object obj); /* = cv.notify_one */
public static void PulseAll(object obj); /* = cv.notify_all */
}

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.

class foo { class foo {


private int a = 0; private int a = 0;
private int b = 0; private int b = 0;

public void MyMethod() { public void MyMethod() {


Monitor.Enter(this); try {
a++; Monitor.Enter(this);
if (a == 12) a++;
b++; if (a == 12)
Monitor.Exit(this); b++;
} }
} finally {
Monitor.Exit(this);
}
}
}

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;

public void MyMethod() {


lock(this) {
a++;
if (a == 12)
b++;
}
}
}
Gerarchia WaitHandle
Le classi per la sincronizzazione sono sottoclassi di WaitHandle, che dentro di sé ha i se-
guenti metodi

public abstract class WaitHandle {


public virtual IntPtr Handle{get; set;}
public virtual bool WaitOne();
public static bool WaitAll(WaitHandle []handles);
public static int WaitAny(WaitHandle []handles);
}

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)

public sealed class Mutex:WaitHandle {


...
public override bool WaitOne(); /* Acquisisco il mutex */
public void ReleaseMutex(); /* Rilascio il mutex */
...
}

class foo {
private int a = 0;
private int b = 0;
private Mutex MyMutex = new Mutex();

public void MyMethod() {


try {
MyMutex.WaitOne();
a++;
if (a == 12)
b++;
}
finally {
MyMutex.ReleaseMutex();
}
}
}

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.

public sealed class ManualResetEvent : WaitHandle


public sealed class AutoResetEvent : WaitHandle {
...
public override bool WaitOne();
public void Set();
public void Reset();
...
}

Gli oggetti ManualResetEvent devono essere resettati (= non segnalati) manualmente.


Tutti i thread in attesa (Wait) vengono svegliati non appena l’evento viene segnalato. Vi-
ceversa, gli oggetti AutoResetEvent si accendono quando qualcuno fa Set e restano ac-
cesi fino a che nessuno è in attesa sull’evento: sono utilizzabili per modellare il proble-
ma del produttore-consumatore tra processi diversi.

Altre classi relative al threading


Ci sono poi tutta una serie di classi relative al threading, tra cui System.Threading.Th-
readPool, che incapsula un insieme di thread già fatti. Il vantaggio di usare un Thread-
Pool è che non dobbiamo occuparci di trovare un thread per fargli fare il nostro mestie-
re. Si possono poi citare i namespace System.Threading.ReaderWriterLock, System.Th-
reading.Interlocked e System.Threading.Timer.

***

You might also like