Introduction

Modern applications don’t use the Thread class from the System.Threading directly. If you see it in one of your projects, it indicates that you are maintaining a legacy code. Is it worth to learn Thread API? Well, it is another topic to discuss. As we already know, .NET 1.0 introduced the Thread API but using it was not so easy. So, why it is better not to use it?

  1. Thread API is the underlying API that directly allows you to manage everything. This option sounds good but having more control over something that you don’t know well may end up with frustration.
  2. It wasn’t an easy task to master Thread API. It has a lot of background history that requires you to dive into the details of OS to fully cover it. Well, sounds a bit scary )
  3. Thread API forces you to think in terms of Threads, not tasks. You as a developer, should think about making your code concurrent, rather than thinking about creating Threads. Thread is a physical concept that is an implementation detail.
  4. Thread creation is an expensive operation.
  5. Using Thread API adds additional complexity from an understandability and maintainability perspective.

In .NET 2.0, Microsoft introduced ThreadPool class which is better than Thread API. The major improvement forward to Threading was isolating us from the Thread concept. You don’t need to think in terms of Threads. You, as a developer, should think in terms of Tasks to be parallelized.

ThreadPool is a wrapper over your threads.

Thread Pool

Why it is a better option?

  1. Via ThreadPool, you’re no longer “the owner” of Threads. There is no manual creation, no complex operations, no problems related to Thread management, deadlock, etc. Guess what? The biggest problem wasn’t a thread but you :)
  2. ThreadPool manages when and why to create Threads. In simple terms, say we have 5 methods. You want to run them in a multithreading mode. You need 5 methods to run them, but ThreadPool may use 2 or 3 Thread to complete your tasks. The limit is up to the thread count you use in a classical thread API.
    internal class Program
    {
        static void Main(string[] args)
        {
            new Thread(ComplexTask).Start();
            Thread.Sleep(1000);
            new Thread(ComplexTask).Start();
            Thread.Sleep(1000);
            new Thread(ComplexTask).Start();
            Thread.Sleep(1000);
            new Thread(ComplexTask).Start();
            Thread.Sleep(1000);
            Console.ReadLine();
        }
    
        static void ComplexTask()
        {
            Console.WriteLine($"Running complex task in Thread={Environment.CurrentManagedThreadId}");
            //Thread.Sleep(40);
            Console.WriteLine($"Finishing complex task in Thread={Environment.CurrentManagedThreadId}");
        }
    }
    

Here is the result

Running complex task

Every operation is a Thread, and it is not an optimized way of using it.

ThreadPool isolates us from The Thread stuff and provides a simple API to enter multithreading mode. Let’s modify our code and try to understand the value of using ThreadPool instead of Thread directly.

internal class Program
{
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem((x) => ComplexTask());
        Thread.Sleep(1000);
        ThreadPool.QueueUserWorkItem((x) => ComplexTask());
        Thread.Sleep(1000);
        ThreadPool.QueueUserWorkItem((x) => ComplexTask());
        Thread.Sleep(1000);
        ThreadPool.QueueUserWorkItem((x) => ComplexTask());
        Thread.Sleep(1000);

        Console.ReadLine();
    }
    static void ComplexTask()
    {
        Console.WriteLine($"Running complex task in Thread={Environment.CurrentManagedThreadId}");
        //Thread.Sleep(40);
        Console.WriteLine($"Finishing complex task in Thread={Environment.CurrentManagedThreadId}");
    }
}

Running complex

Well, as you realized, ThreadPool uses Thread but in an optimized way. If any of the existing threads are free, ThreadPool will use an already existing one instead of creating a new thread. Wait a second... Is it possible for us to do the same operation? I mean, can we run the same Thread twice? Let’s try.

Thread Pool

You can’t run the same Thread twice but somehow ThreadPool can. ThreadPool can reuse Threads and it helps not to create a Thread every time when you have a block of code to run in multithreading mode. In classical .NET, depending on its version and your CPU architecture the capacity of Worker threads is different.

Worker thread

In my case, I’m using the latest .NET ( .NET 8) with an x64 intel processor, and I have 32767 worker Threads with 1000 completion post Threads.

ThreadPool usually starts with 1 Thread in the pool. Depending on the context, it may automatically increase Threads. You can Check available threads in ThreadPool.

internal class Program
{
    static void Main(string[] args)
    {
        ThreadCalculator();
        for (int i = 0; i < 10; i++)
        {
            ThreadPool.QueueUserWorkItem((x) => ComplexTask());
        }
        Thread.Sleep(15);
        ThreadCalculator();
        Console.ReadLine();
    }
    static void ThreadCalculator()
    {
        ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads);
        Console.WriteLine($"worker threads = {workerThreads}, " +
            $"and completion Port Threads = {completionPortThreads}");
    }
    static void ComplexTask()
    {
        Console.WriteLine($"Running complex task in Thread={Environment.CurrentManagedThreadId}");
        //Thread.Sleep(40);
        Console.WriteLine($"Finishing complex task in Thread={Environment.CurrentManagedThreadId}");
    }
}

Threads

The most used API in ThreadPool is, of course, QueueUserWorkItem. It has a generic version, also.

Well, we have another great concept called completion post Threads that we need to cover.

ThreadPool.QueueUserWorkItem is a method in C# that allows you to schedule tasks for execution in a thread pool. Here's a breakdown of its functionality:

Purpose

How it works

You provide a delegate (like Action or WaitCallback) that represents the work you want to be done.

Optionally, you can pass an object containing data to be used by the delegate.

ThreadPool.QueueUserWorkItem adds the delegate and data (if provided) to the thread pool's queue.

When a thread from the pool becomes available, it picks up the first item from the queue and executes the delegate with the provided data.

Benefits or Why to Use It

Additional notes

The thread pool can dynamically adjust the number of threads based on workload.

Tasks are queued if all threads are busy, ensuring none are dropped.

ThreadPool.QueueUserWorkItem has overloads, including a generic version for type safety and an option to influence thread selection.

Ok, but what about completion port Threads?

The concept of completionPortThreads within the ThreadPool class in C# might not be directly exposed as you might expect.

The ThreadPool class internally manages two types of threads.

Purpose of completion port threads

Asynchronous I/O operations typically involve waiting for network requests, file access, or other external events. Completion port threads efficiently wait for these events using a system mechanism called I/O Completion Ports (IOCP).

When an I/O operation completes, the corresponding completion port thread is notified, and it can then dequeue and process the completed task.

What is the value?

Important Notes

You don't directly control completionPortThreads. The ThreadPool class manages its number dynamically based on the system workload.

Methods like ThreadPool.SetMinThreads and ThreadPool.GetAvailableThreads allow you to indirectly influence the minimum number of worker and completion port threads, but there's no separate control for each type.

Who Uses the ThreadPool in C#?

The ThreadPool is a fundamental mechanism in C# for efficiently managing threads. It provides a pool of worker threads that can be reused by various parts of your application, including:

Benefits of Using the ThreadPool

When to consider alternatives

  1. Long-Running Operations: If your work items are long-running (e.g., several seconds or more), use dedicated threads or the TaskFactory.StartNew method with a custom thread creation option might be more suitable to avoid saturating the ThreadPool and affecting overall application performance.
  2. Specialized Thread Requirements: If your tasks require specific thread priority or affinity, you might need to create dedicated threads with the desired settings.

Conclusion

The ThreadPool is a valuable tool for concurrent programming in C#. It offers a convenient and efficient way to manage threads, especially for short-lived, CPU-bound tasks. By understanding how the ThreadPool works and its appropriate use cases, you can create well-structured and performant C# applications.