Introduction
In the previous article, we learned what is ThreadPool in C# and how it works behind the scenes. We also saw some of the benefits and drawbacks of using ThreadPool for concurrent programming. In this article, we will explore some practical examples of using ThreadPool in C# to perform parallel tasks. We will also discuss some of the challenges and best practices of using ThreadPool in C#.
Using ThreadPool in C#: Practical Examples
ThreadPool is a useful class for executing multiple tasks in parallel without creating and managing individual threads. ThreadPool provides a method called QueueUserWorkItem that allows us to queue a work item (a delegate or a lambda expression) to be executed by a thread from the pool. The work item can accept an optional state object as a parameter. For example, the following code queues two work items to the ThreadPool.
using System;
using System.Threading;
namespace ThreadPoolDemo
{
class Program
{
static void Main(string[] args)
{
// Queue two work items to the ThreadPool
ThreadPool.QueueUserWorkItem(WorkItem1, "Hello");
ThreadPool.QueueUserWorkItem(WorkItem2, 42);
// Wait for the work items to complete
Thread.Sleep(1000);
Console.WriteLine("Main thread exits");
}
// A work item that prints a message
static void WorkItem1(object state)
{
Console.WriteLine("WorkItem1: {0}", state);
}
// A work item that calculates and prints the square of a number
static void WorkItem2(object state)
{
int num = (int)state;
int square = num * num;
Console.WriteLine("WorkItem2: {0} * {0} = {1}", num, square);
}
}
}
The output of the program may vary depending on the order of execution of the work items, but it could look something like this.
WorkItem2: 42 * 42 = 1764
WorkItem1: Hello
Main thread exits
Notice that the main thread will not wait for the work items to finish before exiting. This means that the work items may not complete if the main thread terminates the application. To avoid this, we can use a synchronization mechanism such as a ManualResetEvent or a CountdownEvent to signal the completion of the work items and wait for them in the main thread. For example, the following code uses a CountdownEvent to wait for two work items to finish.
using System;
using System.Threading;
namespace ThreadPoolDemo
{
class Program
{
static void Main(string[] args)
{
// Create a CountdownEvent with an initial count of 2
CountdownEvent countdown = new CountdownEvent(2);
// Queue two work items to the ThreadPool and pass the countdown as the state object
ThreadPool.QueueUserWorkItem(WorkItem1, countdown);
ThreadPool.QueueUserWorkItem(WorkItem2, countdown);
// Wait for the countdown to reach zero
countdown.Wait();
Console.WriteLine("Main thread exits");
}
// A work item that prints a message and signals the countdown
static void WorkItem1(object state)
{
Console.WriteLine("WorkItem1");
CountdownEvent countdown = (CountdownEvent)state;
countdown.Signal();
}
// A work item that prints a message and signals the countdown
static void WorkItem2(object state)
{
Console.WriteLine("WorkItem2");
CountdownEvent countdown = (CountdownEvent)state;
countdown.Signal();
}
}
}
The output of the program may look something like this:
WorkItem1
WorkItem2
Main thread exits
In the above code, the main thread waits for the work items to finish before exiting. This ensures that the work items complete their execution.
Sometimes, we may want to queue a work item that returns a result. For this, we can use the Task class, which represents an asynchronous operation that can return a value. The Task class provides a static method called Run that allows us to queue a work item (a delegate or a lambda expression) to the ThreadPool and return a Task object that represents the operation. The Task object has a property called Result that returns the value of the work item when it completes. For example, the following code queues two work items that return the sum and the product of two numbers.
using System;
using System.Threading.Tasks;
namespace ThreadPoolDemo
{
class Program
{
static void Main(string[] args)
{
// Queue two work items to the ThreadPool using Task.Run and store the Task objects
Task<int> task1 = Task.Run(() => Sum(10, 20));
Task<int> task2 = Task.Run(() => Product(10, 20));
// Wait for the tasks to complete and get the results
int result1 = task1.Result;
int result2 = task2.Result;
Console.WriteLine("Sum: {0}", result1);
Console.WriteLine("Product: {0}", result2);
Console.WriteLine("Main thread exits");
}
// A work item that returns the sum of two numbers
static int Sum(int a, int b)
{
return a + b;
}
// A work item that returns the product of two numbers
static int Product(int a, int b)
{
return a * b;
}
}
}
The output of the program may look something like this,
Sum: 30
Product: 200
Main thread exits
Now the main thread waits for the tasks to complete and gets the results using the Result property. This property blocks the calling thread until the task finishes. Alternatively, we can use the Wait method to wait for one or more tasks to complete, or the ContinueWith method to execute a continuation task when a task finishes.
Thread Safety and ThreadPool
When using ThreadPool, it is important to ensure thread safety, which means that the data and resources shared by multiple threads are accessed in a consistent and correct manner. Thread safety is essential for avoiding race conditions, deadlocks, and other concurrency issues that can lead to unpredictable and erroneous behavior of the application.
One of the common thread safety challenges when using ThreadPool is to synchronize the access to shared data. For example, consider the following code that uses a shared variable counter to count the number of work items completed by the ThreadPool.
using System;
using System.Threading;
namespace ThreadPoolDemo
{
class Program
{
// A shared variable to count the number of work items completed
static int counter = 0;
static void Main(string[] args)
{
// Queue 10 work items to the ThreadPool
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem(WorkItem);
}
// Wait for the work items to complete
Thread.Sleep(1000);
Console.WriteLine("Counter: {0}", counter);
Console.WriteLine("Main thread exits");
}
// A work item that increments the counter
static void WorkItem(object state)
{
// Increment the counter
counter++;
Console.WriteLine("WorkItem: {0}", counter);
}
}
}
The output of the program may vary depending on the order of execution of the work items, but it could look something like this.
WorkItem: 1
WorkItem: 6
WorkItem: 7
WorkItem: 8
WorkItem: 9
WorkItem: 10
WorkItem: 3
WorkItem: 4
WorkItem: 5
WorkItem: 2
Counter: 10
Main thread exits
This output seems correct, but it is not guaranteed. The problem is that the counter++ operation is not atomic, which means that it involves multiple steps: reading the value of counter, adding one to it, and storing the new value back to counter. If two or more threads execute this operation concurrently, they may read the same value of counter, increment it, and overwrite each other's results. This can lead to incorrect and inconsistent values of counter.
To avoid this, we need to use a synchronization mechanism such as a lock statement or an Interlocked method to ensure that only one thread can access the shared variable at a time. For example, the following code uses the Interlocked.Increment method to atomically increment the counter.

Join the conversation! Your thoughts help the community grow.