http://www.developer.com/

Back to article

Create Your Own Thread Pool


November 10, 2008

If you build many .NET applications where you need to perform regular work such as packet processing from a socket, you may have run into some of the drawbacks in the built-in System.Threading.ThreadPool. A major limitation of this class is that it is static, which means that an application can have one and only one ThreadPool. The .NET framework itself also uses this ThreadPool for items such as the Timer in the System.Threading namespace. In the end, this means that if you need to use a thread pool in your application and you choose to use the existing .NET class, you will not have full control over it. To overcome this limitation, the simplest solution is to create your own thread pool.

Support Items

To create the thread pool, you first need to build a WorkItem class used to define work submitted to the thread pool and a delegate to process the work. This code is listed below:

public delegate void WorkDelegate(object WorkObject);

class WorkItem
{
   public object WorkObject;
   public WorkDelegate Delegate;
}

MyThreadPool Class

Next, you need to create a MyThreadPool class, which is the main class used to interact with the thread pool. Listed below are the fields and properties for the MyThreadPool class.

private int m_MinThreads=0;
public int MinThreads
{
   get
   {
      return this.m_MinThreads;
   }
   set
   {
      this.m_MinThreads = value;
   }
}

private int m_MaxThreads=100;
public int MaxThreads
{
   get
   {
      return this.m_MaxThreads;
   }
   set
   {
      this.m_MaxThreads = value;
   }
}

private int m_IdleTimeThreshold=5;
public int IdleTimeThreshold
{
   get
   {
      return this.m_IdleTimeThreshold;
   }
   set
   {
      this.m_IdleTimeThreshold = value;
   }
}

The MinThreads/MaxThreads properties are used to control the number of threads used to process the work. The IdleTimeThreshold is used to determine how many seconds a thread should be allowed to sit idle prior to shutting it down.

Next, you dig further into extending the MyThreadPool class with the following properties.

private Queue<WorkItem> WorkQueue;
public int QueueLength
{
   get
   {
      return WorkQueue.Count();
   }
}

private List<WorkThread> ThreadList;
private Thread ManagementThread;
private bool KeepManagementThreadRunning = true;

The WorkQueue shown above is the main queue used to process work in first-in, first out (FIFO) order. Entries stored in the queue are of type WorkItem as defined above. The ThreadList maintains the current list of active threads used to process WorkItems. The ManagementThread is used for performing control functions on the thread pool, such as shutting down idle threads. Next, you'll take a look at the public method used to submit work to the thread pool.

public void QueueWork(object WorkObject, WorkDelegate Delegate)
{
   WorkItem wi = new WorkItem();
   wi.WorkObject = WorkObject;
   wi.Delegate = Delegate;
   lock (WorkQueue)
   {
      WorkQueue.Enqueue(wi);
   }

   //Now see if there are any threads that are idle
   bool FoundIdleThread = false;
   foreach (WorkThread wt in ThreadList)
   {
      if (!wt.Busy)
      {
         wt.WakeUp();
         FoundIdleThread = true; break;
      }
    }

   if (!FoundIdleThread)
   {
      //See if we can create a new thread to handle the
      //additional workload
      if (ThreadList.Count < m_MaxThreads)
      {
         WorkThread wt = new WorkThread(ref WorkQueue);
         lock (ThreadList)
         {
            ThreadList.Add(wt);
         }
      }
   }
}

The method above, QueueWork, is used to submit work to be processed. The QueueWork method first submits the WorkItem to the WorkQueue. Then, the method tries to locate an idle thread in the ThreadList. If an idle thread was not located and the Thread List count is less than the MaxThreads value, the method attempts to launch a new thread. The advantage of starting additional threads in the QueueWork method is that the thread pool will be able to ramp up the number of threads very quickly as work is submitted. Because you are dealing with multiple threads accessing many of the objects, you need to make sure you take the necessary precautions to lock variable to prevent data corruption in the WorkQueue and ThreadList.

The last critical method in the MyThreadPool class is the ManagementWorker as listed below.

private void ManagementWorker()
{
   while (KeepManagementThreadRunning)
   {
      try
      {
         //Check to see if we have idle thread we should free up
         if (ThreadList.Count > m_MinThreads)
         {
            foreach (WorkThread wt in ThreadList)
            {
               if (DateTime.Now.Subtract(wt.LastOperation).Seconds
                  > m_IdleTimeThreshold)
               {
                  wt.ShutDown();
                  lock (ThreadList)
                  {
                     ThreadList.Remove(wt); break;
                  }
               }
            }
         }
      }
      catch { }

      try
      {
         Thread.Sleep(1000);
      }
      catch { }
   }
}

The ManagementWorker is used to manage the thread pool and to shut down idle threads in the ThreadList. It first ensures you have the minimum number of threads. Then, it checks to see whether the thread has been idle long enough to be shut down. Although it may have been possible to have each worker thread perform the necessary checks, it is far easier to manage the list from a central point. However, it also means that each instance of MyThreadPool will have the overhead of a separate management thread.

WorkThread class

The last class in the thread pool is the WorkThread. The critical method in the WorkThread class is Worker, listed below:

private void Worker()
{
   WorkItem wi;
   while (m_KeepRunning)
   {
      try
      {
         while (m_WorkQueue.Count > 0)
         {
            wi = null;
            lock (m_WorkQueue)
            {
               wi = m_WorkQueue.Dequeue();
            }
            if (wi != null)
            {
               m_LastOperation = DateTime.Now;
               m_Busy = true;
               wi.Delegate.Invoke(wi.WorkObject);
            }
         }
      }
      catch { }

      try
      {
         m_Busy = false;
         Thread.Sleep(1000);
      }
      catch { }
   }
}

The Worker method is at the heart of the ThreadPool; it continuously pulls items from the WorkQueue and performs the actual work. Essentially, the method checks the queue once per second. In the event that items are in the queue, it will process items in the queue until the queue has been exhausted. To perform the work, the method executes each delegate from the WorkItem class above. The method passes the WorkObject from the WorkItem class to the delegate method.

Testing the ThreadPool

With all of the classes built, it is fairly easy to create a simple console app to test the thread pool. The test application is listed below:

static void Main(string[] args)
{
   MyThreadPool tp = new MyThreadPool();

   for (int i = 0; i < 100; i++)
   {
      tp.QueueWork(i, new WorkDelegate(PerformWork));
   }

   Console.ReadLine();
   tp.Shutdown();
}

static private void PerformWork(object o)
{
   int i = (int)o;
   Console.WriteLine("Work Performed: " + i.ToString());
   System.Threading.Thread.Sleep(1000);
   Console.WriteLine("End Work Performed: " + i.ToString());
}

The main method above first creates an instance of the MyThreadPool class. Then, it adds 100 work items into the queue of the thread pool. To ensure the console application continues to run after queueing up the work, it uses the Console.ReadLine() to pause the main thread. For the test, the PerfomWork method writes a couple of lines to the console and tells the thread to sleep for 1 to simulate a second of work. The output of the application should look like the following image:

Figure 1: The Console Application

You now have all of the components needed to create multiple thread pools in your application using the above classes.

Conclusion

Consisting of only a couple of classes, the preceding thread pool is a generic thread pool similar to the built-in System.Threading.ThreadPool class, designed for general use. The above thread pool differs from the built-in class in that it will very quickly ramp up the number of threads to accommodate the workload and slowly close threads when idle. In addition, you have full control over what is processed by the thread pool and you can create multiple thread pools. By incorporating a thread pool into your application, you will allow your application to take better advantage of multiple processing cores. However, remember not to create an excessive number of thread pools, because each instance will have the overhead of a single management thread plus the minimum number of worker threads.

Download the Code

You can download the code that accompanies the article here.

About the Author

Chris Bennett is a manager with Crowe Horwath LLP in the Indianapolis office. He can be reached at 317.208.2586 or chris.bennett@crowehorwath.com.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date