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

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