Area di riferimento
- Implementing service processes, threading, and application domains in a .NET Framework application
- Develop multithreaded .NET Framework applications
- Thread class
- ThreadPool class
- WaitCallback delegat
- ThreadStart delegate and ParameterizedThreadStart delegate
- ThreadState enumeration and ThreadPriority enumeration
Thread Pool
I Thread permetto di eseguire operazioni in modo asincrono con lo scopo di migliorare la responsiveness e la scalabilità di un'applicazione.
La creazione e la distruzione di un thread tuttavia è un'operazione abbastanza costosa in termini di tempo. Avere molti thread comporta uno "spreco" non indifferente di memoria che riduce le performance in quanto il sistema operativo deve effettuare la schedulazione e gestire molti context switch tra i thread in esecuzione.
Il CLR gestisce un proprio gruppo di thread, chiamato appunto Thread Pool, che rende disponibili allo sviluppatore. Ogni processo ha il suo Thread Pool. Internamente il Thread Pool mantiene una coda di richiste di operazioni. Quanto una applicazione vuole eseguire una operazione asincrona può chiamare un metodo che permette di aggiungere una richiesta a questa coda. Ciascuna delle operazioni richieste sarà eseguita all'interno di un thread.
Affidarsi al CLR per la gestione dei thread è vantaggioso in quanto al termine dell'esecuzione di un task, il thread utilizzato non viene distrutto ma messo in attesa di gestire un altra richiesta. Solo se non arrivano richieste per un certo tempo allora il thread viene distrutto. La creazione e la distruzione dei thread nel pool viene gestita da un algoritmo euristico che cerca di mantere un compromesso tra l'avere pochi thread e quindi non sprecare risorse e avere molti thread e sfruttare i vantaggi dei multi-processori e dei processori multi-core.
Con la versione 2.0 del CLR, il numero massimo di thread nel pool per CPU è 25.
Vediamo di seguito un esempio di utilizzo del Thread Pool:
// Tipo utilizzato per passare informazioni al metodo che si vuole eseguire in modo asincrono
class ParametriOperazione
{
private int _tempo;
public int Tempo
{
get
{
return _tempo;
}
}
public ParametriOperazione(int tempo)
{
_tempo = tempo;
}
}
class Program
{
static void Main(string[] args)
{
// Chiedo l'esecuzione di tre operazioni in modo asincrono
ThreadPool.QueueUserWorkItem(operazione, new ParametriOperazione(2000));
ThreadPool.QueueUserWorkItem(operazione, new ParametriOperazione(1000));
ThreadPool.QueueUserWorkItem(operazione, new ParametriOperazione(3000));
Console.WriteLine("Fine del Main Thread");
Console.ReadKey();
}
// Operazione che si desidera eseguire in modo asincrono
// La sua firma deve rispettare il delegate definito in System.Threading.WaitCallback
static void operazione(object state)
{
ParametriOperazione parametri = state as ParametriOperazione;
if ( parametri != null)
{
Console.WriteLine("Inizio operazione con parametro {0}", parametri.Tempo);
// Sospendo il Thread per un certo tempo (espresso in millisecondi)
Thread.Sleep(parametri.Tempo);
Console.WriteLine("Fine operazione con parametro {0}", parametri.Tempo);
}
}
}
L'esempio semplicemente richiede per tre volte l'esecuzione asincrona del codice contenuto nel metodo operazione(). Viene utilizzata la classe ParametriOperazione per passare il numero di millisecondi che si desidera sospendere il thread che gestirà l'operazione richiesta. L'output di questa applicazione non sarà sempre lo stesso, ma dipenderà dalle velocità relative di esecuzione dei diversi thread in gioco.
Invece di utilizzare il metodo ThreadPool.QueueUserWorkItem, per migliorare le performance è possibile chiamare il metodo ThreadPool.UnsafeQueueUserWorkItem. Questa funzione biapassa i permessi di sicurezza tipici della Code Access Security (che vedremo più avanti) lasciando un potenziale buco nella sicurezza dell'applicazione. E' opportuno quindi usare con attenzione questo metodo.
Thread
Per i vantaggi spiegati prima è sempre preferibile utilizzare il thread pool per eseguire operazioni asincrone.
Tuttavia, ci sono alcune occasioni in cui è necessario creare direttamente un thread dedicato:
- se si vuole eseguire codice con un livello di priorità speciale (i thread nel pool vengono invece eseguiti tutti con priorità normale)
- se si vuole eseguire operazioni molto lunghe
- se si vuole eseguire una operazione che si desidera abortire prematuramente oppure se si desidera attendere la sua terminazione (metodo Join)
Vediamo come riscrivere il Main dell'esempio precedente costruendo direttamete i thread:
static void Main(string[] args)
{
// Costruisco tre thread
Thread thread1 = new Thread(new ParameterizedThreadStart(operazione));
Thread thread2 = new Thread(operazione);
Thread thread3 = new Thread(operazione);
// Imposto la priorità dei thread
thread1.Priority = ThreadPriority.Normal;
thread2.Priority = ThreadPriority.Normal;
thread3.Priority = ThreadPriority.Highest;
// Mando in esecuzione i thread
thread1.Start(new ParametriOperazione(2000));
thread2.Start(new ParametriOperazione(1000));
thread3.Start(new ParametriOperazione(3000));
// Visualizzo le informazioni sullo stato dei thread
Console.WriteLine("Stato del thread1: {0}", thread1.ThreadState);
Console.WriteLine("Stato del thread2: {0}", thread2.ThreadState);
Console.WriteLine("Stato del thread3: {0}", thread3.ThreadState);
Thread.Sleep(1500);
Console.WriteLine("Stato del thread1: {0}", thread1.ThreadState);
Console.WriteLine("Stato del thread2: {0}", thread2.ThreadState);
Console.WriteLine("Stato del thread3: {0}", thread3.ThreadState);
// Attendo la terminazione dei thread
thread1.Join();
thread2.Join();
thread3.Join();
Console.WriteLine("Stato del thread1: {0}", thread1.ThreadState);
Console.WriteLine("Stato del thread2: {0}", thread2.ThreadState);
Console.WriteLine("Stato del thread3: {0}", thread3.ThreadState);
Console.WriteLine("Fine Main Thread");
Console.ReadKey();
}
Ecco uno dei possibili output del codice precedente:
Inizio operazione con parametro 3000
Inizio operazione con parametro 2000
Inizio operazione con parametro 1000
Stato del thread1: Running
Stato del thread2: WaitSleepJoin
Stato del thread3: WaitSleepJoin
Fine operazione con parametro 1000
Stato del thread1: WaitSleepJoin
Stato del thread2: Stopped
Stato del thread3: WaitSleepJoin
Fine operazione con parametro 2000
Fine operazione con parametro 3000
Stato del thread1: Stopped
Stato del thread2: Stopped
Stato del thread3: Stopped
Fine Main Thread