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

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