How to Use ThreadPool in .Net

  • google plus

In Brief...

The steps below describe how to use the ThreadPool class in .Net.

In order to understand the ThreadPool class, we need to have a solid understanding of threading in general. Let's look at some examples of threading applications in order to understand why we need the ThreadPool Class.

Instructions

First, let's consider a single threaded application. Imagine that we have a warehouse application. Multiple client applications make calls to a central server to increment, decrement, or otherwise modify stock amounts. When these calls are received from the client, the server processes each request synchronously (i.e., each request is processed in turn). As a result, when many requests arrive at the same time, they have to wait for their turn to be processed. This can result in blocking (long wait times) at the client application.

We can resolve the blocking by using asynchronous calls (i.e., creating threads). In the multithreaded version of our warehouse application, when a request arrives at the server, the server creates a thread to process the task. This allows many tasks to be executed simultaneously, but this can also create a new set of problems. The process of creating and destroying threads is expensive and adds quite a bit of overhead to the application. Additionally, during stress periods (high workload) the large number of threads can result in 100% processor usage on the server and again result in long wait times for the clients.

The ThreadPool class resolves these issues. ThreadPool creates and manages a collection, or "Pool," of threads. Because the threads have already been created, there is no overhead incurred when requests are processed. ThreadPool simply takes a thread from the pool and assigns it the task. When the task is complete, the thread is put back into the pool for reuse. ThreadPool determines the optimum number of threads for the application based on several factors (for example, Virtual Address Space available). When the maximum number of threads (as determined by the ThreadPool object) is reached, additional requests will be placed in a queue to wait until a thread becomes available to process it. Thus, our application can use multiple threads without the risk of overwhelming the processor.

Now that we understand WHY we need ThreadPool, Let's examine how to use it. There are several ways to use ThreadPool. In this How-To, we will examine two of the most popular methods - using the Task Parallel Library (TPL) and using QueueUserWorkItem.

First, let's take a look at the TPL approach. We implement TPL using the Task class in the System.Threading.Tasks namespace. The Task class uses the ThreadPool class implicitly.

  1. We begin by bringing in the appropriate namespaces. C#
    
    using System.Threading;
    using System.Threading.Tasks;
    
    VB.Net
    
    Imports System.Threading
    Imports System.Threading.Tasks
    
  2. Next, we create the code to be executed on the thread. This can be a method that has a void return type and accepts a single object as a parameter, or it can be an Action delegate. C#
    
    //Create an Action delegate to execute on the thread
    Action<object> act = (object obj) =>
    {
        Console.WriteLine("Thread={0}, Task={1}, object={2}", Thread.CurrentThread.ManagedThreadId, Task.CurrentId, obj);
    };
    
    OR
    
    //This is a method with a void return type that recieves an object parameter.
    //This method can also be executed using the Task class
    static void TaskToExecute(object obj)
    {
        Console.WriteLine("Thread={0}, Task={1}, object={2}", Thread.CurrentThread.ManagedThreadId, Task.CurrentId, obj);
    }
    
    VB.Net
    
    'Create an Action delegate to execute on the thread
    Dim act As Action(Of Object) =
    Sub(obj As Object)
        Console.WriteLine("Thread={0}, Task={1}, object={2}", Thread.CurrentThread.ManagedThreadId, Task.CurrentId, obj)
    End Sub
    
    OR
    
    Shared Sub TaskToExecute(obj As Object)
        Console.WriteLine("Thread={0}, Task={1}, object={2}", Thread.CurrentThread.ManagedThreadId, Task.CurrentId, obj)
    End Sub
    
  3. Now, we instantiate the Task, passing the code (method or action) to be executed on the thread. When we create the Task, we can either start the task immediately, or we can create it first and then start it after the fact. C#
    
    //Let's create a Task without starting it
    Task task1 = new Task(act, "Task1");
    
    //Now we will create a task and start it
    //Note, this is passing the method instead of the Action delegate!
    Task task2 = Task.Factory.StartNew(TaskToExecute, "Task2");
    
    //Now we will start Task1
    task1.Start();
    Console.WriteLine("Task1 has been started - Main thread ID is{0}", Thread.CurrentThread.ManagedThreadId);
    
    VB.Net
    
    'Let's create a Task without starting it
    Dim task1 As Task = New Task(act, "task1")
    
    'Now we will create a task and start it
    'Note, this is passing the method instead of the Action delegate!
    Dim task2 As Task = Task.Factory.StartNew(AddressOf TaskToExecute, "task2")
    
    'Now we will start Task1
    task1.Start()
    Console.WriteLine("Task1 has been started - Main thread ID is {0}", Thread.CurrentThread.ManagedThreadId)
    
  4. Depending on the code being executed on the thread, we might want to pause (block) the main thread until the task (or tasks) complete. We do this using the Wait method on the Task object. C#
    
    //Now we will tell the main thread to wait until the tasks are completed  This will block the 
    //main thread until both of the tasks have completed.
    task1.Wait();
    task2.Wait();
    
    VB.Net
    
    'Now we will tell the main thread to wait until the tasks are completed  This will block the 
    'main thread until both of the tasks have completed.
    task1.Wait()
    task2.Wait()
    
  5. If desired, we can use the Task class to run a method synchronously. C#
    
    //We can also run a task synchronously
    Task task3 = new Task(act, "task3");
    task3.RunSynchronously();
    //This will block the main thread but it is still a best practice
    //to go ahead and issue a wait, just in case.
    task3.Wait();
    
    VB.Net
    
    'We can also run a task synchronously
    Dim task3 As Task = New Task(act, "task3")
    task3.RunSynchronously()
    'This will block the main thread but it Is still a best practice
    'to go ahead And issue a wait, just in case.
    task3.Wait()
    
  6. We can also use the Task class to run anonymous delegates on a separate thread. C#
    
    //We can also construct an anonymous delegate and run it on a seperate thread
    string tdata = "task4";
    Task task4 = Task.Run(() =>
    {
        Console.WriteLine("Thread={0}, Task={1}, object={2}", Thread.CurrentThread.ManagedThreadId, Task.CurrentId, tdata);
    });
    
    VB.Net
    
    'We can also construct an anonymous delegate and run it on a separate thread
    Dim tdata As String = "task4"
    Dim task4 As Task = Task.Run(
    Sub()
        Console.WriteLine("Thread={0}, Task={1}, object={2}", Thread.CurrentThread.ManagedThreadId, Task.CurrentId, tdata)
    End Sub)
    

Now, let's take a look at how to use the static method QueueUserWorkItem on the ThreadPool Class.

  1. First, we need to create the code that we want to run on a separate thread. We could, of course, use an Action delegate or an anonymous method as we did in the previous example, but for simplicity and clarity, let's just create a simple static method. C#
    
    static void TaskToRun(object obj)
    {
        Console.WriteLine("This task is running in the ThreadPool");
    }
    
    VB.Net
    
    Shared Sub TaskToRun(obj As Object)
        Console.WriteLine("This task is running in the ThreadPool")
    End Sub
    
  2. Now, we simply call the QueueUserWorkItem method on the ThreadPool class and pass the method that we want to run. C#
    
    //Queue a Task
    ThreadPool.QueueUserWorkItem(new WaitCallback(TaskToRun));
    
    VB.Net
    
    'Queue a Task
    ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf TaskToRun))
    
  3. If we have a need to pass information into the Task, we can create a wrapper object to hold the data. C#
    
    public class InfoForTask
    {
        public string value1;
        public int value2;
    
        public InfoForTask(string textIn, int numberIn)
        {
            value1 = textIn;
            value2 = numberIn;
    
        }
    }
    
    VB.Net
    
    Public Class InfoForTask
        Public value1 As String
        Public value2 As Integer
    
        Public Sub New(textIn As String, numberIn As Integer)
            value1 = textIn
            value2 = numberIn
        End Sub
    End Class
    
  4. And then pass it in as the second parameter of the QueueUserWorkItem method call. C#
    
    //Queue a task and pass data to it
    InfoForTask IFT = new InfoForTask("This is the Taxt = ", 42);
    ThreadPool.QueueUserWorkItem(new WaitCallback(TaskToRun), IFT);
    
    VB.Net
    
    'Queue a task and pass data to it
    Dim IFT As New InfoForTask("This is the Taxt = ", 42)
    ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf TaskToRunWithData), IFT)
    
  5. Then we can simply display the data from the thread. C#
    
    //Display Task Data
    static void TaskToRunWithData(object obj)
    {
        InfoForTask IFT = (InfoForTask)obj;
        Console.WriteLine(IFT.value1, IFT.value2);
    }
    
    VB.Net
    
    Shared Sub TaskToRunWithData(obj As Object)
        Dim IFT As InfoForTask = CType(obj, InfoForTask)
        Console.WriteLine(IFT.value1, IFT.value2)
    End Sub
    

Author: Michael Osborne

Discuss