CLR – Thread Pool : uso dei worker thread

Una delle possibilità di utilizzo del thread pool è quella di usufruire dei thread al suo interno per poter eseguire in parallelo delle operazione CPU bound, ossia di calcolo puro. Ovviamente, il CLR definisce una dimensione massima del pool, sia in termini di worker thread che di completion port thread. Per quanto concerne i worker thread, i limiti di default sono i seguenti :

  • 1023 nel .Net Framework 4.0 (ambiente a 32 bit);
  • 32768 nel .Net Framework 4.0 (ambiente a 64 bit);
  • 250 per core nel .Net Framework 3.5;
  • 25 per core nel .Net Framework 2.0;

Tali valori possono essere modificati al ribasso, utilizzando il metodo staticoThreadPool.SetMaxThreads, così come è possibile definirne il numero minimo medianteThreadPool.SetMinThreads. Invece, il numero di thread disponibili nel pool in un dato momento è restituito da ThreadPool.GetAvailableThreads.

Partiamo con una semplice analisi, considerando il seguente applicativo console :

<br />class Program<br />{<br /><%%KEEPWHITESPACE%%>    // numero worker threads e completion port threads<br /><%%KEEPWHITESPACE%%>    private static int workerThreads, completionPortThreads;<br /><br /><%%KEEPWHITESPACE%%>    private const int SCHEDULING_DELAY = 1000;    // delay avvio tra un thread e l'altro<br /><%%KEEPWHITESPACE%%>    private const int NR_WORKER_THREADS = 2;    // numero di thread<br /><%%KEEPWHITESPACE%%>    private const long NR_DATA = 100000000;    // numero di elaborazioni<br /><br /><%%KEEPWHITESPACE%%>    private static int[] inputData;<br /><%%KEEPWHITESPACE%%>    private static int[] outputData;<br /><%%KEEPWHITESPACE%%>    private static ManualResetEvent[] resetEvent;<br /><%%KEEPWHITESPACE%%>    private static Stopwatch sw;<br /><br /><%%KEEPWHITESPACE%%>    static void Main(string[] args)<br /><%%KEEPWHITESPACE%%>    {<br /><%%KEEPWHITESPACE%%>        inputData = new int[NR_DATA];<br /><%%KEEPWHITESPACE%%>        outputData = new int[NR_DATA];<br /><%%KEEPWHITESPACE%%>        resetEvent = new ManualResetEvent[NR_WORKER_THREADS];<br /><%%KEEPWHITESPACE%%>        sw = new Stopwatch();<br /><br /><%%KEEPWHITESPACE%%>        Console.WriteLine("Generating random data...");<br /><%%KEEPWHITESPACE%%>        Random rand = new Random();<br /><%%KEEPWHITESPACE%%>        for (int idx = 0; idx &lt; NR_DATA; idx++)<br /><%%KEEPWHITESPACE%%>        {<br /><%%KEEPWHITESPACE%%>            inputData[idx] = rand.Next(10);<br /><%%KEEPWHITESPACE%%>        }<br /><br /><%%KEEPWHITESPACE%%>        WriteAvailableThreads();<br /><%%KEEPWHITESPACE%%>        sw.Start();<br /><br /><%%KEEPWHITESPACE%%>        Console.WriteLine("Start scheduling works...");<br /><%%KEEPWHITESPACE%%>        for (int j = 0; j &lt; NR_WORKER_THREADS; j++)<br /><%%KEEPWHITESPACE%%>        {<br /><%%KEEPWHITESPACE%%>            resetEvent[j] = new ManualResetEvent(false);<br /><%%KEEPWHITESPACE%%>            ThreadPool.QueueUserWorkItem(DoWork, j);<br /><%%KEEPWHITESPACE%%>            Thread.Sleep(SCHEDULING_DELAY);<br /><%%KEEPWHITESPACE%%>        }<br /><%%KEEPWHITESPACE%%>        Console.WriteLine("Waiting for all works...");<br /><%%KEEPWHITESPACE%%>        WaitHandle.WaitAll(resetEvent);<br /><%%KEEPWHITESPACE%%>        sw.Stop();<br /><%%KEEPWHITESPACE%%>        Console.WriteLine("All works finished, elapsed time = {0} ms", sw.ElapsedMilliseconds);<br /><br /><%%KEEPWHITESPACE%%>        Console.ReadLine();<br /><%%KEEPWHITESPACE%%>    }<br /><br /><%%KEEPWHITESPACE%%>    private static void DoWork(object state)<br /><%%KEEPWHITESPACE%%>    {<br /><%%KEEPWHITESPACE%%>        int workIdx = (int)state;<br /><%%KEEPWHITESPACE%%>        Console.WriteLine("Start DoWork{0} at {1} ms with ThreadId {2}", workIdx + 1, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);<br /><%%KEEPWHITESPACE%%>        WriteAvailableThreads();<br /><br /><%%KEEPWHITESPACE%%>        for (int idx = 0; idx &lt; NR_DATA; idx++)<br /><%%KEEPWHITESPACE%%>        {<br /><%%KEEPWHITESPACE%%>            outputData[workIdx] += inputData[idx] * inputData[idx];<br /><%%KEEPWHITESPACE%%>        }<br /><br /><%%KEEPWHITESPACE%%>        Console.WriteLine("End DoWork{0} at {1} ms with ThreadId {2}", workIdx + 1, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);<br /><%%KEEPWHITESPACE%%>        resetEvent[workIdx].Set();<br /><%%KEEPWHITESPACE%%>    }<br /><br /><%%KEEPWHITESPACE%%>    private static void WriteAvailableThreads()<br /><%%KEEPWHITESPACE%%>    {<br /><%%KEEPWHITESPACE%%>        ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);<br /><%%KEEPWHITESPACE%%>        Console.WriteLine("workerthreads = {0}, completionPortThreads = {1}", workerThreads, completionPortThreads);<br /><%%KEEPWHITESPACE%%>    }<br />}<br />

di cui abbiamo :

  • un metodo DoWork che simula un’elaborazione CPU bound, eseguendo una semplice computazione sugli elementi dell’array inputData, la cui dimensione è fissata mediante la costante NR_DATA;
  • il Main che ha il compito di lanciare/schedulare un certo numero di elaborazioni parallele eseguite con il medesimo metodo DoWork; il numero è fissato mediante la costante NR_WORKER_THREADS e il delay tra un avvio e l’altro mediante laSCHEDULING_DELAY;
  • l’array resetEvent di ManualResetEvent che serve per la sincronizzazione tra il thread principale e i thread che via via terminano;

Per quanto riguarda l’output, l’applicativo visualizza sulla console i tempi di avvio dei thread, il loro ID (in modo da poter distinguere l’allocazione di un nuovo thread dal pool rispetto ad un altro) ed il numero di worker thread disponibili nel pool.

Modulando i valori delle tre costanti NR_DATA (dimensione array da computare),NR_WORKER_THREADS (numero di elaborazioni parallele) e SCHEDULING_DELAY (delay tra una richiesta di elaborazione e la successiva) è possibile descrivere la variazione del comportamento del CLR nell’utilizzo del thread pool. E’ da precisare che i test, saranno eseguiti su una CPU dual core.

Partiamo con i seguenti valori ed osserviamone il risultato :

  • SCHEDULING_DELAY = 10;
  • NR_WORKER_THREADS = 2;
  • NR_DATA = 100000000;

7077.worker_thread_1_7CA54CFE

SCHEDULING_DELAY = 10; NR_WORKER_THREADS = 2; NR_DATA = 100000000;

Si osserva, sulla base delle due elaborazioni parallele richieste, che vengono allocati due thread separati nel pool (i due ID sono differenti ed il numero di worker thread disponibili è decrementato di due rispetto la condizione iniziale) e che vengono eseguiti parallelamente dai due core della CPU (il tempo di esecuzione è pressoché lo stesso).

Riduciamo drasticamente il tempo necessario all’elaborazione portando il valore NR_DATA a 1000.

1004.worker_thread_2_618C7DF0

SCHEDULING_DELAY = 10; NR_WORKER_THREADS = 2; NR_DATA = 1000;

In questo caso, osserviamo che l’elaborazione è così rapida che il CLR tende ad eseguire le due richieste in maniera sequenziale riutilizzando il medesimo thread (stesso ID). In questo caso, sarà stato utilizzato un solo core della CPU. Ovviamente, una situazione simile si ottiene aumentando il delay tra un avvio e l’altro considerando un tempo di elaborazione lungo, in quanto si dà al CLR il tempo di riciclare lo stesso thread nel pool.

4555.worker_thread_3_47B847C1

SCHEDULING_DELAY = 1000; NR_WORKER_THREADS = 2; NR_DATA = 100000000;

Proviamo ad aumentare il numero di elaborazioni parallele portandole a 4. E’ ovvio che, avendo a disposizione una CPU dual core, sarà possibile parallelizzare realmente solo 2 computazioni ed eseguire più context switch per fornire il multithreading per le altre elaborazioni. Ovviamente il CLR tenderà ad utilizzare più pool nel thread, se assumiamo un tempo di elaborazione piuttosto lungo. Un possibile output sarà il seguente.

2526.worker_thread_4_78D78261

SCHEDULING_DELAY = 10; NR_WORKER_THREADS = 4; NR_DATA = 100000000;

In questo caso, due elaborazioni vengono avviate quasi contemporaneamente e per la terza viene allocato un nuovo thread. Al termine della prima elaborazione, viene avviata la quarta riciclando il thread di quella appena terminata (ID = 11).

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s