CLR

Windows Embedded Compact 2013 : firmiamo un assembly per il .Net Compact Framework 3.9 da includere nella GAC

Con questo post, inizia un breve tutorial sull’utilizzo di una libreria proprietaria (da noi sviluppata con .Net Compact Framework 3.9) su Windows Embedded Compact 2013, dalla firma dell’assembly (per la registrazione nella GAC) fino al progetto di esempio, passando per la realizzazione di un componente esposto dal Platform Builder (nel catalog items) e da poter includere banalmente nell’immagine del sistema operativo.

In particolare vedremo :

  1. Come eseguire la firma di un assembly;
  2. Creazione di un componente proprietario per il catalog items di Platform Builder;
  3. Distribuzione ed inclusione del componente nell’immagine del sistema operativo;
  4. Supporto per il debugging dell’applicazione che utilizzerà il componente;
  5. Realizzazione di una semplice applicazione di esempio;

Prenderò come riferimento il mio progetto M2Mqtt (MQTT Client library for .Net), supponendo che sia esso il componente da voler includere nell’immagine, in modo che terze parti possano sviluppare un’applicazione che lo utilizzi avendolo già onboard su un target device con Windows Embedded Compact 2013 e .Net Compact Framework 3.9.

La firma : GAC, Public Key Token e Strong Name

Molto spesso si pone la necessità di includere un proprio assembly (magari una libreria da noi sviluppata) direttamente in un’immagine di Windows Embedded Compact, così come viene incluso il .Net Compact Framework grazie al Platform Builder (utilizzando semplicemente il catalog items). In questo modo, la DLL si troverà nella cartella \Windows del target device (insieme a tutti gli assembly del framework) e sarà utilizzabile da applicazione terze parti solo registrandola nella GAC (Global Assembly Cache).

Nel caso del .Net Compact Framework, per eseguire la verifica dell’assembly al suo caricamento dalla GAC, è necessario che esso sia firmato ed abbia un Public Key Token associato. Il Public Key Token è un hash a 64 bit della chiave pubblica relativa alla chiave privata utilizzata per firmare l’assembly. E’ utile per rendere un assembly univoco in modo che due assembly con lo stesso nome vengano considerati distinti (ecco perché si parla di Strong Name). Considerando come riferimento la mia libreria M2Mqtt per il .Net Compact Framework 3.9, vediamo quali sono i passaggi necessari per la firma dell’assembly corrispondente.

Usiamo lo Strong Name Tool

In primo luogo è necessario generare una coppia di chiavi privata/pubblica e per questa operazione possiamo utilizzare lo Strong Name Tool (sn.exe) fornito con Visual Studio. Lanciamo il “Developer Command Prompt for VS2012” ed eseguiamo il seguente comando :

sn –k M2Mqtt.snk

Firmiamo l’assembly

Il passo successivo è quello di utilizzare questo file .snk per la firma dell’assembly in modo da generare un Public Key Token corrispondente. Generalmente, un progetto basato su .Net Framework ha la tab “Signing” nella finestra delle “Properties” in cui è possibile selezionare il file.

8712.01_thumb_3FCD4E6E

Nel caso di un progetto per il .Net Compact Framework 3.9, questa tab non è disponibile ma è necessario intervenire sul file AssemblyInfo.cs specificando al suo interno la linea seguente :

[assembly:AssemblyKeyFileAttribute(“M2Mqtt.snk”)]

Il file .snk va copiato nella stessa cartella in cui si trova quello di progetto (.csproj).

Attraverso questa impostazione, al termine della compilazione avremo un assembly firmato (in questo caso M2Mqtt.dll) che possiamo ispezionare utilizzando un tool come il Reflector della RedGate.

1374.02_thumb_1826D244

Abbiamo l’assembly firmato e pronto per essere incluso nell’immagine del sistema operativo. Nel post successivo vedremo come sia possibile realizzazione un componente visibile nel catalog items di Platform Builder che includa il nostro file e tutta la relativa configurazione.

Sviluppo in .Net su processori ARM

Vi segnalo questo interessantissimo articolo “.Net Development for ARM processors” di Andrew Pardoe (Program Manager nel CLR team) su MSDN Magazine che esplora l’evoluzione dello sviluppo in .Net dal processore x86 al processore ARM, con l’introduzione del .Net Compact Framework ed il .Net Micro Framework fino all’avvento della piattaforma in Windows 8.

Sono evidenziate le notevoli differenza tra le due architetture di cui deve tener conto il team che sviluppa il CLR in modo che il passaggio da un’architettura all’altra sia trasparente per noi sviluppatori.

L’ho trovato molto…molto…interessante….leggere per credere !

Codice Nativo vs Gestito : performance

Affrontando un problema lavorativo riguardo le perfomance grafiche su un target device con processore ARM e Windows CE 6.0 R3 e facendo delle ricerche di approfondimento in rete, mi sono imbattuto in un interessantissimo articolo di Chris Tacke dell’OpenNETCF Communty intitolato “Native vs. Managed Code : GDI Performance”.

L’articolo, attraverso un’applicazione di test, dimostra quanto il codice nativo non possa essere considerato in assoluto molto più veloce del codice gestito ma soltanto per una percentuale relativa che però può talvolta variare da piattaforma a piattaforma. In particolare, il test viene eseguito utilizzando le GDI di Windows.

Un ulteriore interessante risultato è che l’utilizzo di metodi del .Net Compact Framework che fungono da wrapper di corrispondenti metodi nativi (vedi ad esempio Graphics.FillRectangleche è wrapper dell’API FillRect) riducono le performance rispetto all’invocazione diretta delle API di sistema attraverso le P/Invoke. E’ ovvio che il wrapper al suo interno eseguirà sempre una P/Invoke ma aggiunge un carico di lavoro in più, probabilmente legato a dei check che vengono eseguiti prima della chiamata nativa (vedi anche il marshalling dei parametri).

CLR – Thread Pool : elaborazioni periodiche con i timer

Capita spesso di dover eseguire periodicamente un’elaborazione ad intervalli prefissati. Una prima soluzione, assolutamente non efficace, potrebbe essere quella di predisporre un thread che al termine dell’elaborazione stessa va in sleep per un tempo pari al periodo richiesto, per poi rieseguire la computazione; tutto ciò in un opportuno ciclo. Considerando quanto detto una semplice idea e comunque un approccio non ottimale, la migliore soluzione la possiamo ottenere attraverso la classe Timer (del namespace System.Threading) che fa ampio uso del Thread Pool.

Considerando il costruttore più ampio di tale classe, esso prevede i seguenti parametri :

  • una delegate del tipo TimerCallback che rappresenta la callback da invocare ogni qual volta scatta il timer;
  • un oggetto con eventuali informazioni di stato da passare alla callback ogni qual volta viene invocata;
  • il dueTime, ossia il ritardo con il quale la callback viene invocata per la prima volta (specificando il valore 0, la callback viene invocata subito);
  • il periodo di invocazione della callback. Specificando Timeout.Infinite, viene disabilitata la periodicità e la callback sarà invocata solo una volta;

L’utilizzo di questa classe è strettamente legato al Thread Pool, in quanto il CLR utilizza uno dei worker thread del pool per invocare la callback associata al timer stesso. Ciò vuol dire che per più timer, impostati con periodi diversi e comunque aventi elaborazioni che non si sovrappongono nel tempo, il CLR è in grado di riciclare il medesimo thread per eseguire le corrispondenti callback con un notevole risparmio di risorse. E’ altresì ovvio che, qualora le elaborazioni delle callback si “sovrapponessero” nel tempo, il CLR sarebbe costretto ad allocare thread differenti per poterle eseguire.

Consideriamo la seguente semplice applicazione :

class Program
{

    // numero worker threads e completion port threads
    private static int workerThreads, completionPortThreads;

    private const int SCHEDULING_DELAY = 1000;    // delay avvio tra un thread e l'altro
    private const int NR_TIMER = 4;    // numero di thread
    private const long NR_DATA = 100000000;    // numero di elaborazioni

    private static Stopwatch sw;

    static void Main(string[] args)
    {
        Timer[] timers = new Timer[NR_TIMER];
        sw = new Stopwatch();

        WriteAvailableThreads();
        sw.Start();

        for (int idx = 0; idx < NR_TIMER; idx++)
        {
            timers[idx] = new Timer(TimerWork, idx, 5000, 1000);
            Thread.Sleep(SCHEDULING_DELAY);
        }
        Thread.Sleep(30000);

        for (int idx = 0; idx < NR_TIMER; idx++)
        {
            timers[idx].Dispose();
        }

        sw.Stop();
        Console.WriteLine("All works finished, elapsed time = {0} ms", sw.ElapsedMilliseconds);
        WriteAvailableThreads();

        Console.ReadLine();
    }

    private static void TimerWork(object state)
    {
        Console.WriteLine("Start TimerWork{0} at {1} ms with ThreadId {2}", state, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);
        WriteAvailableThreads();

        long sum = 0;
        for (int i = 0; i < NR_DATA; i++)
        {
            sum += i;
        }

        Console.WriteLine("End TimerWork{0} at {1} ms with ThreadId {2}", state, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);
    }

    private static void WriteAvailableThreads()
    {
        ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
        Console.WriteLine("workerthreads = {0}, completionPortThreads = {1}", workerThreads, completionPortThreads);
    }
}

Attraverso la quale creiamo NR_TIMER timer con un delay l’uno rispetto l’altro pari a SCHEDULING_DELAY e supponiamo che la callback associata ai timer esegua un’elaborazione (una banalissima somma) con un certo numero di dati pari a NR_DATA. Assumiamo che il dueTime sia 5000 ms e che il timer scatti una sola volta e non periodicamente.

Di seguito un primo risultato.

2376.timer1_34E65D33

SCHEDULING_DELAY = 1000, NR_TIMER = 4, NR_DATA = 10000000

Si osserva che schedulando abbastanza lentamente l’avvio dei timer, allo scattare delle relative callback e considerando la velocità dell’elaborazione, il CLR riesce a riciclare sempre il medesimo worker thread.

Proviamo a velocizzare la schedulazione dei timer.

4401.timer2_60B6B122

SCHEDULING_DELAY = 10, NR_TIMER = 4, NR_DATA = 10000000

In questo caso, il CLR è costretto ad utilizzare due worker thread per eseguire le callback previste per i quattro timer.

Aggiungendo la periodicità al timer (in questo caso l’ho settata a 1000 ms), la situazione diventa non molto predicibile e complessa. Dallo screenshot che segue si evince come ad un certo punto il CLR abbia avuto la necessità di allocare 6 worker thread (da 1023 disponibili a 1017) per gestire 4 timer. Ciò è dovuto al fatto che, quando un worker thread è occupato e non ce ne sono altri già creati e disponibili, il CLR è costretto ad allocarne uno nuovo e che quelli già presenti nel pool vengono eliminati, se non usati, solo dopo un certo tempo di inattività.

3443.timer3_58BF0EC0

SCHEDULING_DELAY = 1000, NR_TIMER = 4, NR_DATA = 10000000, PERIOD = 1000

CLR – Thread Pool : attesa di eventi con i wait handlers

Capita spesso di dover eseguire una certa elaborazione al verificarsi di un evento. Ovviamente, non faccio riferimento agli eventi scatenati sulla UI (es. click di un bottone,..) ai quali la gestione è demandata ai relativi event handlers, ma mi riferisco ad eventi intrinseci dell’applicazione che magari servono per sincronizzare uno o più thread fra loro (es. sbloccare un thread nel momento in cui un altro thread ha terminato una computazione ed ha reso disponibili dei dati).

La soluzione tipica è quella di creare un WaitHandle (concretamente un AutoResetEvent oManualResetEvent in base alle esigenze) ed un thread, dopodichè bloccare quest’ultimo mediante una WaitOne() sull’evento stesso. In questo modo, il thread va in wait e si sbloccherà nel momento in cui “qualcuno” (un altro thread o il main thread) eseguirà unaSet() sull’evento. Questa tecnica, prevede comunque la creazione di un thread custom da parte dello sviluppatore.

Ebbene, anche in una situazione di questo tipo, ci viene fornito supporto dal thread pool del CLR. Infatti, utilizzando il metodo statico RegisterWaitForSingleObject() della classeThreadPool, è possibile demandare al CLR il monitoring di un evento e far eseguire una callback nel caso in cui quest’ultimo diventi signaled o nel caso in cui scatti un timeout.

Il metodo RegisterWaitForSingleObject prevede i seguenti parametri :

  • un WaitHandle, ossia l’evento da monitorare;
  • la callback da eseguire nel momento in cui l’evento viene segnalato o scatta il timeout. La callaback è definita attraverso il delegate WaitOrTimerCallback;
  • un oggetto di stato da passare alla callback (utile per trasferire dati a quest’ultima);
  • un timeout in millisecondi allo scattare del quale la callback viene comunque invocata nonostante l’evento non sia stato segnalato;
  • una flag che indica se il monitoring dell’evento deve essere ripetuto nel tempo oppure se va eseguito una sola volta (nel cui caso al signaling dell’evento o alla scattare del timeout, l’evento non verrà più monitorato);

Infine, esso ritorna un RegisteredWaitHandle sul quale va eseguito il metodo diUnregister() nel momento in cui non è più necessario, in modo da liberare le risorse allocate per l’evento. E’ da osservare che il delegate WaitOrTimerCallback prevede un primo parametro che rappresenta lo stato eventualmente passato alla callback ed un secondo parametro booleano che indica se la callback è stata chiamata per timeout o meno.

Consideriamo la seguente applicazione console :

class Program
{
    // numero worker threads e completion port threads
    private static int workerThreads, completionPortThreads;

    private const int SCHEDULING_DELAY = 1000;
    private const int NR_WAIT_CALLBACK = 2;
    private const long NR_DATA = 100000000;
    private const int WAIT_TIMEOUT = 5000;
    private const int SET_DELAY = 1000;

    private static AutoResetEvent[] waitEvent;              // wait handles da monitorare
    private static AutoResetEvent[] syncEvent;              // wait handles per la sincronizzazione con il main thread
    private static RegisteredWaitHandle[] regWaitHandle;    // handle relativi alle risorse allocate dal manager
                                                            // del thread pool per il monitoring
    private static Stopwatch sw;

    static void Main(string[] args)
    {
        waitEvent = new AutoResetEvent[NR_WAIT_CALLBACK];
        syncEvent = new AutoResetEvent[NR_WAIT_CALLBACK];
        regWaitHandle = new RegisteredWaitHandle[NR_WAIT_CALLBACK];
        sw = new Stopwatch();

        WriteAvailableThreads();
        sw.Start();

        Console.WriteLine("Generating wait event...");
        for (int idx = 0; idx < NR_WAIT_CALLBACK; idx++)
        {
            waitEvent[idx] = new AutoResetEvent(false);
            syncEvent[idx] = new AutoResetEvent(false);
            regWaitHandle[idx] = ThreadPool.RegisterWaitForSingleObject(waitEvent[idx], WaitMethod, idx, WAIT_TIMEOUT, true);
            Thread.Sleep(SCHEDULING_DELAY);
        }

        Console.WriteLine("Waiting for all wait methods...");
        WaitHandle.WaitAll(syncEvent);

        sw.Stop();
        Console.WriteLine("All  wait methods finished, elapsed time = {0} ms", sw.ElapsedMilliseconds);

        for (int j = 0; j < NR_WAIT_CALLBACK; j++)
        {
            regWaitHandle[j].Unregister(waitEvent[j]);
        }

        Console.ReadLine();
    }

    private static void WriteAvailableThreads()
    {
        ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
        Console.WriteLine("workerthreads = {0}, completionPortThreads = {1}", workerThreads, completionPortThreads);
    }

    private static void WaitMethod(object state, bool timeout)
    {
        int waitMethodIdx = (int)state;
        Console.WriteLine("Start WaitMethod{0} with timeout={1} at {2} ms with ThreadId {3}", waitMethodIdx + 1, timeout, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);
        WriteAvailableThreads();

        int sum = 0;
        for (int i = 0; i < NR_DATA; i++)
        {
            sum += i;
        }
        Console.WriteLine("End WaitMethod{0} at {1} ms with ThreadId {2}", waitMethodIdx + 1, sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId);
        syncEvent[waitMethodIdx].Set();
    }
}

In cui :

  • waitEvent è l’array con i wait handle da monitorare e sui quali saremo in attesa;
  • syncEvent  è l’array con i wait handle per la sincronizzazione tra il main thread ed i thread relativi alle callback eseguite in seguito allo scattare degli eventi o del timeout;
  • regWaitHandle è l’array con i RegisteredWaitHandle usati dal manager del thread pool per gestire il monitoring;

L’applicativo non fa nient’altro che registrare NR_WAIT_CALLBACK in attesa sugli eventi dell’array waitEvent con un tempo di schedulazione di SCHEDULING_DELAY millisecondi. Inoltre, è definito un timeout pari a WAIT_TIMEOUT millisecondi. La prima versione del codice applicativo, prevede che le callback si sblocchino tutte per timeout. Infine, NR_DATA servirà per simulare un’elaborazione nelle callback.

Proviamo ad eseguirla con i seguenti valori :

  • SCHEDULING_DELAY = 100;
  • NR_WAIT_CALLBACK = 2;
  • NR_DATA = 100000000;
  • WAIT_TIMEOUT = 5000;

Otteniamo il seguente output :

1805.wait_handle_1_1B282EDF

SCHEDULING_DELAY = 100, NR_WAIT_CALLBACK = 2, WAIT_TIMEOUT = 5000

In questo caso, la registrazione di wait sull’handle avviene rapidamente (100 ms di delay) e si osserva che, allo scattare del timeout (5 sec) ovviamente le due callback dovranno essere chiamate “quasi” in contemporanea, considerando anche il tempo abbastanza lungo di elaborazione per ciascuna di esse. Per questo motivo, vengono allocati due completion port thread per eseguire tali callback.

Proviamo ad aumentare il delay tra una richiesta di registrazione e l’altra. Otteniamo …

8741.wait_handle_2_78F02358

SCHEDULING_DELAY = 1000, NR_WAIT_CALLBACK = 2, WAIT_TIMEOUT = 5000

In questo caso, la prima callback verrà invocata un secondo prima della seconda callback e considerato il fatto che l’elaborazione termina entro tale secondo, il manager del thread pool riesce a riciclare il medesimo completion port thread per gestirle entrambe. Se proviamo, però, ad aumentare il tempo di elaborazione (portando NR_DATA a 1000000000), la prima callback non sarà ancora terminata quando scatterà il timeout per la seconda, per cui il manager del thread pool sarà costretto ad allocare un nuovo thread.

8154.wait_handle_3_5F1BED29

SCHEDULING_DELAY = 1000, NR_WAIT_CALLBACK = 2, WAIT_TIMEOUT = 5000, NR_DATA = 1000000000

Aggiungiamo ora la condizione di sblocco settando gli eventi. Per fare questo, inseriamo le seguenti righe di codice subito dopo il ciclo di registrazione delle callback :

for (int k = 0; k < NR_WAIT_CALLBACK; k++)
{
    waitEvent[k].Set();
    Thread.Sleep(SET_DELAY);
}

Tra un’operazione di signaling di un evento e la successiva, lasciamo trascorrere un certo tempo per simulare uno sblocco differito. Ovviamente, modulando questo tempo, verrà riciclato o meno lo stesso thread del pool per eseguire la callback :

  • con SET_DELAY piccolo, i signaling sono così ravvicinati e magari l’elaborazione nella callback abbastanza lunga da costringere il CLR ad allocare thread separati nel pool;
  • con SET_DELAY grande, considerando il medesimo tempo di elaborazione nella callback, passa comunque più tempo tra un signaling ed il successivo per cui il CLR ha il tempo di riciclare il medesimo thread;

Riporto di seguito due possibili situazioni

1513.wait_handle_4_733509B2

SET_DELAY=100

0825.wait_handle_5_446F5110

SET_DELAY=1000

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

CLR – Thread Pool : Introduzione

Generalmente, la creazione di un thread è molto costosa in termini di risorse da allocare ed in termini di tempo. Inoltre, avere molti thread attivi nel sistema per poter eseguire molteplici operazioni in parallelo tende a ridurre le performance del sistema stesso, in quando il processore è costretto ad eseguire numerosi context switch durante lo scheduling.

Per ottimizzare l’utilizzo delle risorse e migliorare le performance, ci viene in aiuto il thread pool fornito dal CLR. Bisogna precisare, però, che la funzionalità del thread pool è nativa in Windows e che tutte le classi ed i metodi forniti dal CLR ne rappresentano comunque un wrapper.

1727.image_5B05635A

Attraverso il thread pool, il CLR fornisce appunto un pool di thread che possiamo utilizzare per eseguire elaborazioni in parallelo. Il manager del thread pool ha a disposizione una coda all’interno della quale pervengono tutte le richieste di elaborazione parallela fatte dalla nostra applicazione. Ogni qual volta viene inserita una nuova richiesta, il manager analizza se nel thread pool è disponibile un thread libero in stato di wait e pronto per poter essere assegnato all’esecuzione di una nuova elaborazione. Nel caso affermativo, viene eseguita tale assegnazione e la computazione da noi richiesta viene eseguita all’interno di un thread che deve essere solo avviato e non creato (in quanto già esistente) con un notevole risparmio di tempo (soprattutto in fase di avvio) e di risorse (non dobbiamo allocare il contesto di un nuovo thread). Viceversa, nel caso in cui, un thread non fosse disponibile nel pool, esso viene creato (ovviamente con una riduzione di performance e maggior utilizzo di risorse) ed assegnato all’elaborazione da eseguire. Nel momento in cui, però, un thread del pool termina la sua elaborazione esso non viene eliminato ma rimesso nel pool in stato di wait e pronto per essere assegnato ad altre richieste. L’eliminazione del thread, per risparmiare risorse allocate, viene eseguita solo nel caso in cui un thread non viene utilizzato per un certo tempo definito nel CLR.

Da questo scenario si evince che i vantaggi possono essere :

  • riduzione al minimo dell’allocazione di risorse;
  • velocità nell’avvio di un’elaborazione parallela;
  • miglioramento generale delle performance;

I parametri che possono, però, influenzare l’utilizzo ottimale del pool sono molteplici ed in particolare :

  • frequenza di schedulazione : intesa come la frequenza con cui la nostra applicazione richiede l’avvio di una computazione utilizzando il thread pool. E’ ovvio che se tale frequenza è elevata, ci saranno maggiori possibilità che il manager del pool debba allocare più thread dal pool stesso considerando che la coda delle richieste tende a popolarsi rapidamente. Con una frequenza di schedulazione bassa, probabilmente avremo più possibilità di riutilizzare lo stesso thread per eseguire tutte le richieste pervenute nella coda. E’ anche vero che ciò è legato alla durata della computazione (vedi dopo);
  • durata della computazione : indica la durata dell’elaborazione che vogliamo far eseguire ad un thread del pool. Più essa è alta e più un thread rimane occupato  e quindi il manager del pool dovrà allocare e utilizzare altri thread per esaudire ulteriori richieste nella coda;
  • numero di elaborazioni parallele : inteso come il numero di richieste che la nostra applicazione fa al manager del pool per eseguire elaborazioni parallele. Maggiore è il numero di richieste e maggiore è la probabilità che verranno utilizzati più thread nel pool per esaudirle;
  • numero di core del processore : questo aspetto hardware influisce soprattutto sulle performance, considerando che pur nel caso di più thread allocati dal pool, essi saranno distribuiti su più core e quindi la velocità di esecuzione sarà notevole;

Dalle considerazioni suddette, si evince che, per ottimizzare l’utilizzo di risorse del pool è necessario eseguire delle elaborazioni molto brevi, in modo che il manager sia in grado di riciclare sempre lo stesso thread per eseguirle. E’ anche vero che in questo caso le nostre operazioni verranno “sequenzializzate”. Un miglioramento delle performance a discapito di maggiori risorse allocate, lo otteniamo soprattutto nel caso in cui le computazioni sono più lunghe, per cui il manager è costretto ad allocare più thread, e magari usufruire di un processore multicore in modo da parallelizzare “realmente” le esecuzioni.

E’ ovvio che in questi termini, possono essere fatti numerosi ragionamenti alterando in un modo o nell’altro i parametri suddetti che vanno ad influenzare l’utilizzo ottimale del pool e delle risorse di sistema.

E’ inoltre importante sottolineare che, le computazioni per le quali richiediamo un’elaborazione parallela non devono prevedere dei meccanismi di sincronizzazione o accesso a risorse condivise, altrimenti decade il miglioramento di performance dovuto al parallelismo, soprattutto in un sistema multicore.

Tornando all’implementazione del pool nel CLR, esso fornisce due tipologie di thread :

  • worker threads : utilizzati per operazioni di tipo CPU bound;
  • completion port threads : thread che vengono avviati tipicamente al termine di operazioni asincrone su file o socket. L’uso di tali thread si basa proprio sul meccanismo delle completion port fornito da Windows. Infatti, ogni qual volta viene avviata un’operazione asincrona, uno dei meccanismi forniti da Windows per segnalare il termine di tale operazione, è quello di inserire un pacchetto in una coda specifica opportunamente monitorata da un thread, che nel nostro caso sarà proprio un completion port thread nel quale sarà eseguita la callback prevista al termine dell’operazione asincrona;

Prima di terminare questa breve introduzione per poi “sporcarci” le mani negli articoli successivi, anticipo quelli che saranno i possibili campi di utilizzo del thread pool :

  • worker thread : possibilità di utilizzare un thread per eseguire un’operazione CPU bound;
  • wait event : utilizzare un thread del pool per monitorare lo stato di un evento e di far eseguire una nostra callback nel momento in cui l’evento risulta segnalato oppure un eventuale timeout è scattato;
  • timer : uso di un thread per eseguire operazioni periodiche basate appunto su un timer;
  • I/O asincrono : possibilità di lanciare operazioni asincrone su file o socket per poi demandare ad un thread del pool il compito di eseguire una callback al termine di tale operazione;