Thread Pool

Now you have AppDomains that have been described as lightweight processes, and Threads that are contained in AppDomains. It is time to introduce a lighter still Thread that is spawned from the ThreadPool class. Starting up a Thread takes little overhead, but even a little overhead is some overhead. Starting a Thread from a pool of Threads removes much of the startup costs associated with starting a Thread from scratch. The Thread is queued, and the system (the CLR) determines when and with what resources it should be run.

QueueUserWorkItem

The Thread that is queued by QueueUserWorkItem is part of a pool and it can be assigned to some other work immediately after completing the work that you assigned it to do. Therefore, you are not notified when the Thread completes its assignment, or more precisely, when the work that has been queued up has completed. Listing 11.15 shows the relevant code for using QueueUserWorkItem. The full source to this sample is available as part of the ThreadPool solution in ThreadPoolQueueUserWorkItem.

Listing 11.15. ThreadPool.QueueUserWorkItem
private void OnStart(object sender, System.EventArgs e)
{
    fibonacciResultsDelegate = new FibonacciResultsDelegate(OnFibonacciResults);
    Fibonacci fib = new Fibonacci(0);
    try
    {
        fib.Start();
        ThreadPool.QueueUserWorkItem(new WaitCallback(fib.Compute), this);
    }
    catch (NotSupportedException)
    {
        Debug.WriteLine("NotSupportedException was caught because: ");
        Debug.WriteLine("	System.ThreadPool.QueueUserWorkItem Not Supported on this
 system.");
        Debug.WriteLine("	Must be running on Win2K, or the extra Win32 Support");
    }
}

When the user presses the Start button, a delegate is created, the Fibonacci series is initialized to start at 0, and the work of computing the Fibonacci number is queued up using QueueUserWorkItem. When a Thread is assigned to the queued work from the ThreadPool, then the Compute method of the Fibonacci class is called. On entry to this function, the Fibonacci number is computed, the elapsed time calculated, the results are reported to the UI, and the next Fibonacci calculation is queued.

This sample looks like Figure 11.9 after it has been run.

Figure 11.9. Testing QueueUserWorkItem.


How much lighter is the Thread started from the ThreadPool and a “normal” Thread? That is difficult to answer because you are not guaranteed to even get a Thread when invoking QueueUserWorkItem. As the name implies, the work is “queued,” not necessarily started. I built an application that tries to quantify when it is best to use the ThreadPool and when it is best to start a Thread manually. At first, the numbers did not seem to be correct because I was expecting a Thread for each work item that was queued. I looked at the Task Manager and the Performance Monitor and noticed that if I queued up 100 work items, at no time did I get 100 Threads to service these work items. What is up?

The ThreadPool works off a set pool of about 25 Threads. When no more Threads are available, the work item is queued waiting for a Thread to become available to service the work. I came to the realization that the ThreadPool is a finite system-wide resource that should be used with care. If my application uses up all of the available Threads in the pool for a long time, then the ThreadPool's overall performance degrades significantly. The other issue that I learned in developing this benchmark application is that the savings over doing the same work in a Thread becomes smaller as the task or work to be performed takes longer. In other words, the ThreadPool should be used for performing short tasks. The benchmark program is available as part of the ThreadPool solution in ThreadPoolBenchmark. When the application is run, it looks like Figure 11.10.

Figure 11.10. Benchmark for QueueUserWorkItem.


You need to understand the benchmark program a bit further. First, the work that is performed is computing a Fibonacci number. The number and the amount of work that is performed do not have a linear correspondence; it is almost an exponential relationship. For example, the work needed to compute a Fibonacci number for 30 is less than half the work required to compute a Fibonacci number for 31. Computing Fibonacci numbers for values greater than 40 (at least on my computer) takes a long time.

Second, it probably is not useful to increase the iterations too high. This input is merely provided to smooth over any spikes that might occur in the benchmark.

Third, the number of Threads only has meaning for the Thread test case. What this really means is the number of times that the work will be performed. With a ThreadPool, there will not necessarily be a Thread per work item. This will definitely not be the case if the number of work items queued is larger than about 25.

Try running this benchmark with a fixed “thread count” of 10 and an iteration count of 10 for both of the tests. With both of these numbers fixed, slowly increase the work from about 10 to 35 or 40 (depending on your patience). The average elapsed time for the ThreadPool Test moves up closer to the average elapsed time for the Thread Test. This shows again that QueueUserWorkItem should be used for performing short tasks because its benefits become smaller as the length of the task increases.

RegisterWaitForSingleObject

The method RegisterWaitForSingleObject allows the programmer to perform the same operation that is available with WaitHandle functions such as WaitOne, but in an asynchronous fashion. The first argument to RegisterWaitForSingleObject is the object for which to wait. If the object becomes signaled before the timeout period specified, then the callback specified by the WaitOrTimerCallback delegate will be called with the second argument specifying that a timeout has not occurred. If a timeout occurs (the amount of time specified by the timeout argument to RegisterWaitForSingleObject has elapsed and the object passed as the first argument has not been signaled yet), then the callback is called with the second argument indicating that a timeout has occurred.

A sample has been put together that illustrates the usage of ThreadPool.RegisterWaitForSingleObject. Unlike the previous sample that arbitrarily limited the values for which a Fibonacci number was computed to 40, this sample computes values until computing the number takes longer than 5 seconds.

When this sample is run, it looks like Figure 11.11. The full source to this sample is available as part of the ThreadPool solution in ThreadPoolRegisterWaitForSingleObject.

Figure 11.11. RegisterWaitFor SingleObject.


The relevant code is shown in Listing 11.16 and in Listing 11.7.

Listing 11.16. ThreadPool.RegisterWaitForSingleObject
private void OnStart(object sender, System.EventArgs e)
{
    fibonacciList.Items.Clear();
    fibonacciResultsDelegate = new FibonacciResultsDelegate(OnFibonacciResults);
    Fibonacci fib = new Fibonacci(0, 5000);
    WaitOrTimerCallback callback = new WaitOrTimerCallback(fib.Result);
    try
    {
        fib.Event.Reset();
        Thread t = new Thread(new ThreadStart(fib.Compute));
        t.Start();
        ThreadPool.RegisterWaitForSingleObject(fib.Event, callback, this, fib.Timeout, true);
    }
    catch (NotSupportedException)
    {
        Debug.WriteLine("NotSupportedException was caught because: ");
        Debug.WriteLine("	System.ThreadPool.RegisterWaitForSingleObject Not Supported on
 this system.");
        Debug.WriteLine("	Must be running on Win2K, or the extra Win32 Support");
    }
}

When the user presses the Start button, all the items in the results list are cleared, a delegate is created, and the Fibonacci class is constructed with a start value of 0 and a timeout value of 5000 milliseconds. The Fibonacci class also creates a ManualResetEvent that is used to signal that a result is ready. Next, a callback is constructed for RegisterWaitForSingleObject. This delegate must have a signature specified by WaitOrTimerCallback. Then the “all done” event is Reset. This event will be signaled by the Thread performing the work. Next, the Thread that will be performing the work is started. Finally, a callback is registered to be called when the event is signaled or when a timeout occurs. As shown in Listing 11.17, the process is repeated if no timeout occurs.

Listing 11.17. ThreadPool.RegisterWaitForSingleObject
public void Compute()
{
    Start();
    result = fib(fibnumber);
    Stop();
    jobstart.Set();
}
public void Result(Object o, bool timedout)
{
    if(!timedout)
    {
        RegisterWaitForSingleObjectForm form = (RegisterWaitForSingleObjectForm)o;
        form.BeginInvoke(form.Results, new object[]{fibnumber, result, Elapsed} );
        fibnumber++;
        jobstart.Reset();
        Thread t = new Thread(new ThreadStart(Compute));
        t.Start();
        ThreadPool.RegisterWaitForSingleObject(jobstart, new WaitOrTimerCallback(Result),
 o, timeout, true);
    }
}

Listing 11.17 also shows the entry point for the Thread that is performing the work.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.5.239