Thread Pooling

Whenever you start a thread, a few hundred microseconds are spent organizing such things as a fresh private local variable stack. Each thread also consumes (by default) around 1 MB of memory. The thread pool cuts these overheads by sharing and recycling threads, allowing multithreading to be applied at a very granular level without a performance penalty. This is useful when leveraging multicore processors to execute computationally intensive code in parallel in “divide-and-conquer” style.
 
The thread pool also keeps a lid on the total number of worker threads it will run simultaneously. Too many active threads throttle the operating system with administrative burden and render CPU caches ineffective. Once a limit is reached, jobs queue up and start only when another finishes. This makes arbitrarily concurrent applications possible, such as a web server.
There are a number of ways to enter the thread pool:

  • Via the Task Parallel Library or PLINQ (from Framework 4.0)
  • By calling ThreadPool.QueueUserWorkItem
  • Via asynchronous delegates
  • Via BackgroundWorker

Entering the Thread Pool via TPL

You can enter the thread pool easily using the Task classes in the Task Parallel Library. These were introduced in Framework 4.0: if you’re familiar with the older constructs, consider the nongeneric Task class a replacement for ThreadPool.QueueU serWorkItem, and the generic Task a replacement for asynchronous delegates. The newer constructs are faster, more convenient, and more flexible than the old.
 
To use the nongeneric Task class, call Task.Factory.StartNew, passing in a delegate of the target method:

using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
    class Program
    {
        static void Main() // The Task class is in System.Threading.Tasks
        {
            Task.Factory.StartNew(Go);
        }
        static void Go()
        {
            Console.WriteLine("Hello from the thread pool!");
        }
    }
}

Task.Factory.StartNew returns a Task object, which you can then use to monitor the task—for instance, you can wait for it to complete by calling its Wait method.

Entering the Thread Pool Without TPL

You can’t use the Task Parallel Library if you’re targeting an earlier version of the .NET Framework (prior to 4.0). Instead, you must use one of the older constructs for entering the thread pool: ThreadPool.QueueUserWorkItem and asynchronous delegates. The difference between the two is that asynchronous delegates let you return data from the thread. Asynchronous delegates also marshal any exception back to the caller.

To use QueueUserWorkItem, simply call this method with a delegate that you want to run on a pooled thread:

using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            ThreadPool.QueueUserWorkItem(Go);
            ThreadPool.QueueUserWorkItem(Go, 123);
            Console.Read();
        }
        static void Go(object data) // data will be null with the first call.
        {
            Console.WriteLine("Hello from the thread pool! " + data);
        }
    }
}

Our target method, Go, must accept a single object argument (to satisfy the WaitCallback delegate). This provides a convenient way of passing data to the method, just like with ParameterizedThreadStart. Unlike with Task, QueueUserWor kItem doesn’t return an object to help you subsequently manage execution. Also, you must explicitly deal with exceptions in the target code—unhandled exceptions will take down the program.

Asynchronous delegates

ThreadPool.QueueUserWorkItem doesn’t provide an easy mechanism for getting return values back from a thread after it has finished executing. Asynchronous delegate invocations (asynchronous delegates for short) solve this, allowing any number of typed arguments to be passed in both directions. Furthermore, unhandled exceptions on asynchronous delegates are conveniently rethrown on the original thread (or more accurately, the thread that calls EndInvoke), and so they don’t need explicit handling.
 
Here’s how you start a worker task via an asynchronous delegate:
1. Instantiate a delegate targeting the method you want to run in parallel (typically one of the predefined Func delegates).
2. Call BeginInvoke on the delegate, saving its IAsyncResult return value. BeginInvoke returns immediately to the caller. You can then perform other activities while the pooled thread is working.
3. When you need the results, call EndInvoke on the delegate, passing in the saved IAsyncResult object.
 
In the following example, we use an asynchronous delegate invocation to execute concurrently with the main thread, a simple method that returns a string’s length:

using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            Func<string, int> method = Work;
            IAsyncResult cookie = method.BeginInvoke("test", null, null);
            //
            // ... here's where we can do other work in parallel...
            //
            int result = method.EndInvoke(cookie);
            Console.WriteLine("String length is: " + result);
        }
        static int Work(string s) 
        { 
            return s.Length; 
        }
    }
}

EndInvoke does three things. First, it waits for the asynchronous delegate to finish executing, if it hasn’t already. Second, it receives the return value (as well as any ref or out parameters). Third, it throws any unhandled worker exception back to the calling thread.