C#Programmazione

Multithreading C#

Ogni applicazione di una certa complessità necessita di gestire ed eseguire più compiti in contemporanea. Un thread è un percorso di esecuzione di un processo all’interno di una stessa applicazione, il primo thread viene creato dal sistema operativo all’avvio dell’applicazione. Ogni thread può avviare thread secondari per eseguire compiti in parallelo.

Per avviare un nuovo thread si usa il metodo Start che avvierà il codice indicato dal ThreadStart.

Thread th = new Thread(new ThreadStart(CodiceThread));
th.Start();

Tramite il metodo Join il thread primario attende che il thread secondario finisca l’esecuzione, possiamo inserire come parametro un valore di timeout per far si che il thread primario riparta anche se il secondario non ha completato l’esecuzione.

Possiamo anche modificare la priorità nei vari Thread tramite la proprietà Priority:

  • Lowest
  • BelowNormal
  • Normal
  • AboveNormal
  • Highest

Generalmente i thread sono impostati come foreground e quindi l’applicazione rimane attiva fino a quando tutti i thread sono in esecuzione, possiamo impostare la proprietà IsBackground come true in modo che quanto termina il thread primario l’applicazione viene chiusa anche se ci sono altri thread in esecuzione.

Possiamo passare dei parametri al nuovo thread tramite il metodo start:

Thread th = new Thread(new ThreadStart(CodiceThread));
string hello = “Hello!”;
th.Start(hello);

Per superare il limite di un solo parametropossiamo usare l’espressioni lambda

string hello = “Hello!”;
int n = 5;
int interval = 1000;
Thread th = new Thread(()=> {
for(inti=0; i<n; i++)
{
Console.WriteLine(hello);
Thread.Sleep(interval);
}
});

Avere più thread in esecuzione contemporaneamente può creare diversi problemi; se più thread accedono alla stessa risorsa si ha una race condition mentre se più thread sono in attesa uno dell’altro si ha una deadlock, ma possiamo avere altre problematiche.

lock permette di definire un blocco in modo che un thread che inizia a eseguire tale blocco non può essere interrotto da un’altro prima di averlo completato. Si basa su un tipo riferimento che viene utilizzato come token per poter entrare all’interno della sezione, un secondo thread dovrà attenderne il rilascio.

lock(obj)
{
sezione critica
}

Possiamo usare Interlocked per casistiche molto semplici per operazioni atomiche, ad esempio per l’incremento di una variabile possiamo usare Interlocked.Increment(ref n);

L’istruzione loocker è equivalente al metodo Monitor.Try che permette di definire anche un tempo massimo (Monitor.TryEnter) che un thread deve attendere (tramite un TimeSpan in millisecondi) prima di entrare nella sezione critica.

int timeOut = 1000;
bool lockTaken = false;
Monitor.TryEnter(obj, timeOut, ref lockTaken);
if(lockTaken)
{
try
{
sezione critica
}
finally
{
Monitor.Exit(obj);
}
}
else
{
lock non acquisito
}

Molto simile è la classe Mutex che può essere conosciuto a livello di sistema operativo. Per creare un Mutex bisogna indicare se il thread vuole da subito acquisire un Mutex, assegnare un eventuale nome (se non viene assegnato nessun nome non sarà condiviso con altri processi a livello di sistema operativo) e farsi restituire come parametro d’uscita un valore booleano che indica se l’oggetto è stato creato correttamente. Il metodo WaitOne viene utilizzato come controllore di accesso a una sezione critica. All’uscita del blocco occorre invocare ReleaseMutex.

Se il codice è molto semplice possiamo usare un thread pool, esso crea e mantiene un insieme di thread pronti all’uso; il metodo per accodare attività da eseguire QueueUserWorkItem.

Il metodo Thread per implementare applicazioni multithreading oltre ad essere soggetto a diverse limitazioni riguardanti il passaggio di parametri è molto gravoso dal punto di vista delle performance. Per venire in contro allo sviluppatore è stato introdotto il concetto di task, che è un’astrazione di livello più alto rispetto ai thread.

La Task Parallel Library è un insieme di tipi e API per facilitare lo sviluppo di applicazioni che fanno uso di attività parallele e concorrenti.

Per Avviare un nuovo task possiamo utilizzare il metodo Task.Factory.StartNew(()); oppure il metodo Run ch semplifica ancora di più la creazione e l’esecuzione del task.

Possiamo conoscere lo stato di un Task è possibile esaminare la proprietà Status:

  • Running – in esecuzione;
  • RanToCompletation – completato con successo;
  • IsCompleted – completato correttamente;
  • IsFaulted – terminato per il verificarsi di un’eccezione;
  • IsCancelled – annullato;

Il metodo Wait blocca l’esecuzione del codice fino a quando il task in esame non è terminato, un overload permette di specificare un timeout per terminare anticipatamente un task.

Possiamo anche decidere che un task, al termine del suo compito, deve riestituire un valore:

Task<string> task = Task<string>.Run(() =>
{

}

In questo esempio viene restituita una stringa.

Un task può essere annullato prima del suo completamento, ecco come avviarlo

var CancTSource = new CancellationTokenSource();
CancellationTokenSource token = cancTSource.Token;
Task task Canc = Task.Run(()=>
{

}, token);

Possiamo decidere che il successivo task parte solo al completamento del precedente tramite il metodo ContinueWith, oppure ritardare l’avvio tramite il metodo statico Delay.

Con WhenAll<TResult> se vogliamo che un task parte dopo il completamento di tutti i task restituendo un valore eventualmente, mentre con WhenAny il task parte quando uno dei task è stato completato.

I Task si possono nidificare, il Task padre può avere due tipi di Task figlio:

  • detached, (predefinito) viene eseguito indipendentemente dal padre;
  • attached, sincronizzato con l’esecuzione del padre, eventuali eccezioni vengono propagate al padre. La creazione dei task figli deve essere fatta con l’opzione TaskCreationOption.AttachedToParent;

Se all’interno di un task si genera un’eccezione, questa viene propagata al punto in cui si attende il completamento del task. L’eccezione viene gestita all’interno del blocco try/catch.

Le eccezioni vengono inglobate in AggregateException e sono accessibili tramite la proprietà InnerExceptions e li possiamo scorrere tramite il metodo Handle.

Possiamo verificare eventuali eccezioni che hanno causato il fallimento del task tramite la proprietà IsFaulted.

Un metodo sincrono, detto bloccante, blocca il programma fino al termine della sua esecuzione. Al contrario un metodo asincrono restituisce subito il controllo al chiamante e nel frattempo svolge il suo lavoro. Tramite il metodo run il task viene avviato senza bloccare il chiamante, tranne che vogliamo attendere utilizzando il metodo Wait.
fine