Efficiently process jobs in parallel in C# .NET using thread pools. Use the built-in framework ThreadPool class, while constraining threads and updating a ProgressBar incrementally. Tap multicore architecture for batch processing or expensive tasks.
The .NET framework provides us with the System.Threading namespace, which includes the ThreadPool class. This is a static class that you can access directly. It provides us with the essential parts of thread pools.
It is an implementation of the common "thread pool" design pattern. It is useful for running many separate tasks in the background. There are better options for a single background thread (see below).
This is usually entirely useless to know. The whole point of ThreadPool in .NET is that it internally manages the threads in the ThreadPool. Multicore machines will have more threads than older machines.
From Microsoft. "Thread pools typically have a maximum number of threads. If all the threads are busy, additional tasks are placed in queue until they can be serviced as threads become available."
On servers and in batch processing applications. ThreadPool has internal logic that makes getting a thread much less expensive. This is because the threads are already made and are just "hooked up" when required. Here's why ThreadPool-style code is used on servers.
What Microsoft says. "Thread pools are often employed in server applications. Each incoming request is assigned to a thread from the thread pool, so the request can be processed asynchronously, without tying up the primary thread or delaying the processing of subsequent requests." [How to Use a Thread Pool - MSDN]
If you are using Windows Forms, prefer the BackgroundWorker for more simple threading requirements. BackgroundWorker does well with network accesses and other simple stuff. For batch processing with many processors, you need ThreadPool. [C# - BackgroundWorker Thread Use - dotnetperls.com]
| If your app | Then consider |
| Does batch processing | ThreadPool |
| Makes many (3+) threads | ThreadPool |
| Uses Windows Forms | BackgroundWorker |
Thread considerations. Also, the specifics of how you use your threads can help you find the best code. This next table compares the threading scenarios and which class is best.
| If | Then use |
| You need one extra thread | BackgroundWorker |
| You have many short-lived threads | ThreadPool |
Threading is important but for most apps that don't take a long time to execute and are only doing one thing, it is not important. For applications whose interface usability isn't important, avoid threads as well.
By using QueueUserWorkItem. You have your method you want to run on the threads, and you must hook it up to QueueUserWorkItem. How can you do this? You must use WaitCallback.
What does WaitCallback do? At MSDN, WaitCallback is described as a delegate callback method to be called when the ThreadPool executes. It is a delegate that "calls back" its argument.
By simply using the "new WaitCallback" syntax as the first argument to ThreadPool.QueueUserWorkItem. You don't need any other code here.
void Example()
{
// Hook up the ProcessFile method to the ThreadPool.
// Note: 'a' is an argument name. Read more on arguments.
ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessFile), a);
}
private void ProcessFile(object a)
{
// I was hooked up to the ThreadPool by WaitCallback.
}By defining a special class and putting your important values inside of it. Then, the object is received by your method, and you can cast it. Here's an example that builds on the earlier ones.
// Special class that is an argument to the ThreadPool method.
class ThreadInfo
{
public string FileName { get; set; }
public int SelectedIndex { get; set; }
}
class Example
{
public Example()
{
// Declare a new argument object.
ThreadInfo threadInfo = new ThreadInfo();
threadInfo.FileName = "file.txt";
threadInfo.SelectedIndex = 3;
// Send the custom object to the threaded method.
ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessFile), threadInfo);
}
private void ProcessFile(object a)
{
// Constrain the number of worker threads
// (Omitted here.)
// We receive the threadInfo as an uncasted object.
// Use the 'as' operator to cast it to ThreadInfo.
ThreadInfo threadInfo = a as ThreadInfo;
string fileName = threadInfo.FileName;
int index = thread.SelectedIndex;
}
}What's going on. We are sending two values to the ProcessFile threaded method. It needs to know the FileName and the SelectedIndex, and we send all this in the object parameter.
By adding the Windows Forms control in the Toolbox panel on the right to your Windows program in the designer. Next, you have to deal with progressBar1.Value, progressBar1.Minimum, and progressBar1.Maximum. The Value is your position between the minimum and the maximum. Initialize your ProgressBar like this:
// Set progress bar length. // Here we have 6 units to complete, so that's the maximum. // Minimum usually starts at zero. progressBar1.Maximum = 6; // or any number progressBar1.Minimum = 0;
ProgressBar position. The length of the colored part of your ProgressBar is the Value's percentage of the Maximum. So, if the Maximum is 6, a Value of 3 will be halfway done.
Unfortunately, you can't access Windows controls on worker threads, as the UI thread is separate. We have to use a delegate and Invoke onto the ProgressBar.
public partial class MainWindow : Form
{
// This is the delegate that runs on the UI thread to update the bar.
public delegate void BarDelegate();
// The form's constructor (autogenerated by Visual Studio)
public MainWindow()
{
InitializeComponent();
}
// When a buttom is pressed, launch a new thread
private void button_Click(object sender, EventArgs e)
{
// Set progress bar length.
progressBar1.Maximum = 6;
progressBar1.Minimum = 0;
// Pass these values to the thread.
ThreadInfo threadInfo = new ThreadInfo();
threadInfo.FileName = "file.txt";
threadInfo.SelectedIndex = 3;
ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessFile), threadInfo);
}
// What runs on a background thread.
private void ProcessFile(object a)
{
// (Omitted)
// Do something important using 'a'.
// Tell the UI we are done.
try
{
// Invoke the delegate on the form.
this.Invoke(new BarDelegate(UpdateBar));
}
catch
{
// Some problem occurred but we can recover.
}
}
// Update the graphical bar.
private void UpdateBar()
{
progressBar1.Value++;
if (progressBar1.Value == progressBar1.Maximum)
{
// We are finished and the progress bar is full.
}
}
}Delegate syntax. Near the start of the above code, you will see the delegate UpdateBar declared. This syntax is strange but it tells Visual Studio and C# that you need to use the method as an object.
More work needed. The above program demonstrates how you can set the Maximum and Minimum on the ProgressBar, and how you can Invoke the delegate method after the work is done to increment the size of the ProgressBar.
Here I show how to look at the threads in the Visual Studio 2008 debugger. Once you have a program working, you can take these steps to visualize the threads.
Four worker threads. The above image shows ten threads total, but four of the worker threads are the ones in my program that are assigned to MainWindow.ProcessFile.
If you have a dual-core or quad-core system, you will want at most 2 or 4 demanding threads. We can do this by keeping a _threadCount field and tracking the number of running threads.
Lock required. With this thread count field, you will need to use a lock in C# to avoid having the field read or written incorrectly. Locks shield your thread from changes in other threads.
// Lock on this object.
readonly object _countLock = new object();
private void ProcessFile(object argument)
{
// Constrain the number of worker threads
while (true)
{
// Prevent other threads from changing this under us
lock (_countLock)
{
if (_threadCount < 4)
{
// Start the processing
_threadCount++;
break;
}
}
Thread.Sleep(50);
}
// Do work...
}What we see. The above code is the method executed asynchronously. It will not start its work until there are fewer than 4 other worker threads. This is good for a quad-core machine.
Progress bars and fast UIs on Windows Forms applications are very impressive and not extremely difficult. However, threads introduce lots of complexity and lead to bugs. ThreadPool is a useful simplification, but it is still difficult.
More material on ThreadPool. You can use SetMinThreads on ThreadPool to improve the throughput and performance in bursts of activity. I have material on the best number of minimum threads to use. [C# - ThreadPool Throughput Improvement - dotnetperls.com]