Month: May 2011

WP7 Mango : qualche link utile

5710.wp7_mango_fetta_thumb_29827258

Sono state annunciate da poco le novità della prossima versione “Mango” di Windows Phone 7 e la Microsoft ha rilasciato in versione Beta dell’SDK. Di seguito riporto alcuni link utili per iniziare ad apprezzare le nuove funzionalità.

Come installare Windows Phone Developer Tools 7.1 Beta (Mango)

Free Training Course su WP7 Mango

Alcuni video sui Mango Developer Tools :

Advertisements

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

ASP.NET 4 : video corsi gratuiti dalla Pluralsight

Sul sito ufficiale di ASP.NET sono stati messi a disposizione una serie di corsi gratuti su ASP.NET 4 Web Forms ed ASP.NET MVC 3 da parte della Pluralsight, società di formazione partner della Microsoft.

Ovviamente tutti rigorosamente in lingua inglese ma penso che ne valga la pena seguirli. E’ necessario il plugin Silverlight per la loro visione.

Buona visione !

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