There are a variety of ways to add parallelism to your .Net application. I won’t go into detail about the benefits of doing so, suffice to say that if your application is executing multiple tasks that can run in parallel, it can benefit from being multithreaded.
In order to implement parallelism in your application you have to consider the synchronisation of your threads. Done incorrectly, which is very easy to do, and you can waste a lot of time debugging non-deterministic issues in your application or even make it slower than a synchronous version of your application.
I am going to write three posts to accompany my multithreaded application. The first post will describe the various multithreading methods used. The second post will illustrate the synchronisation methods used in the code. Finally the third post will cover testing the multithreaded application along with detailing other notable multithreading methods that weren’t used and why.
The Application
The scenario for the multi-threaded application involves three threads. One thread populates a collection with random integer values every second. Another thread removes a random entry from the collection and squares it before re-inserting it back in the collection every two seconds. Finally the last thread prints the total sum of the collection every three seconds.
Threading Methods
Thread
The Thread class in the System.Threading namespace is the most basic type that gives you control of a single thread of execution in your program. You specify the logic that you want the thread to execute through the ThreadStart delegate that you pass to the constructor:
var newThread = new Thread(new ThreadStart(/*DelegateToExecute*/));
Constructing a thread involves spending a few hundred microseconds organizing things such as a fresh private local stack. Also each thread instance consumes around 1Mb of memory by default.
ThreadPool
In order to avoid having to create a new Thread instance each time you can use the ThreadPool class which is again in the System.Threading namespace. This type allows you to create a pool of threads that can be re-used for processing many tasks rather than creating a new thread each time. The easiest way to execute some logic via the ThreadPool is to call the QueueUserWorkItem method:
ThreadPool.QueueUserWorkItem(new WaitCallback(/*MethodToExecute*/));
You can specify the number of threads that get created by the ThreadPool through SetMaxThreads, or let it default to a number calculated based on various factors such as the size of the virtual address space. You can call GetMaxThreads to find out this number. All ThreadPool threads are background threads and as a result won’t prevent your application from exiting if they are active.
Task
The benefit of using Tasks, from the System.Threading.Tasks namespace, over Threads or the ThreadPool is that they are more efficient and provide a more scalable use of resources. The Task Parallel Library (TPL) uses Tasks in order to release developers from the burden of dealing with low level details such as specifying which threads to use, the scheduling of these threads, and providing cancellation support. There are a few ways to create tasks. The methods from the TPL weren’t used because I needed greater control over the Tasks. Therefore my options included using either Task.Run or Task.Factory.StartNew:
1 2 |
var newTask = Task.Run(new Action(ActionToExecute)); var newTask = Task.Factory.StartNew(new Action(ActionToExecute)); |
Task.Factory.StartNew provides the most capacity for the customisation of Tasks by providing you control over the scheduling of the Task through the TaskCreationOptions that you can specify in some of the overloads. I didn’t need that extent of customisation so I plumped for Task.Run. One thing to note is if you have a long running Task it is best to set the LongRunning flag to true. This is a hint to the scheduler which then might assign it a dedicated thread outside of the ThreadPool.
BackgroundWorker Component
If you are writing a GUI then you can use an instance of the BackgroundWorker class from the System.ComponentModel namespace. The BackgroundWorker communicates with the main UI thread via events in order to report progress and signal completion. In order to execute a BackgroundWorker you need to assign an event handler for the DoWork event that gets processed when the RunWorkerAsync is called on the BackgroundWorker instance:
1 2 3 4 |
void DoWorkMethod(object sender, DoWorkEventArgs e) { /* execute some logic */ } var newWorker = new BackgroundWorker(); newWorker.DoWork += DoWorkMethod; newWorker.RunWorkerAsync(); |
One limitation is that you can’t use BackgroundWorkers to communicate across AppDomains. Also you can’t have nested BackgroundWorkers.
Summary
As you can see there is a wide variety of threading methods available in .Net. I chose to use the ones that I thought were most appropriate for solving the requirements of the program. In the next post I will describe the various synchronisation methods used in order to ensure that my threads were executed in a specific order.