Part 1 introduced us to the basic of concurrent programming in .NET. In this series, we will move from .NET 1.0 to 4.0 and see how concurrency can be achieved using the evolving API. Today, we look at the most basic explicit threading class.
public sealed class Thread : CriticalFinalizerObject, _Thread
Interestingly, the Thread class started to inherit from CriticalFinalizerObject and implement _Thread interface only since .NET framework 2.0. In .NET 1.0 and 1.1, it merely inherited from System.Object. The _Thread interface exposes the managed thread object to unmanaged code. Why this wasn’t the case before .NET 2.0 is beyond me. CriticalFinalizerObject is more interesting as it enforces the so-called Constrained Execution Region automatically in the derived class’s object finalizer (destructor in C#) code. Essentially, it means it will execute the finalizer even if the application domain is unloading or the thread is aborted. You can read all the glory details here.
When creating a new thread object, there are 4 flavors: a combination of whether the delegate to execute has parameters and whether there is a maximum stack size rule on the new thread. We will look at these options in detail.
- ThreadStart is a simple delegate that has no parameter and doesn’t have a return value. .NET delegates has no counterpart in Java. In C/C++, there is something called function pointer, which allows the caller to pass function objects around. Microsoft likes to claim that delegates are superior because they are type-safe. In details, all delegates are implemented as classes. Therefore, delegates are not first-class citizens in .NET. We will re-use the code from part 1 here.
public class HelloRunnable {
public static void run() {
System.Console.WriteLine("Hello from a thread!");
}
public static void Main(string [] args) {
new System.Threading.Thread(run).Start();
}
}
If you notice, there is no mention of any ThreadStart delegate in the code! This is a nice syntactic sugar introduced in .NET 2.0. Instead of instantiating an object of type ThreadStart like this
public class HelloRunnable
{
public static void run()
{
System.Console.WriteLine("Hello from a thread!");
}
public static void Main(string[] args)
{
System.Threading.ThreadStart startDelegate = new System.Threading.ThreadStart(run);
new System.Threading.Thread(startDelegate).Start();
}
}
You get to pass in method name directly in the place of a delegate in the constructor.
- If you’ve thinking about the need to pass in parameters so that the new thread can use, ParameterizedThreadStart is your friend. It takes in an object as the parameter that gives you a lot of flexibility (e.g. you can pass in an entire hash table if you like) at the cost of type-safety. Since every classes derive from System.Object, the compiler won’t catch any error if you try to cast the parameter down to a more derived object. Maybe some sort of generic ParameterizedThreadStart with optional parameters (new in .NET 4.0) can help here.
public class HelloRunnable
{
public static void run(object message)
{
System.Console.WriteLine(message);
}
public static void Main(string[] args)
{
System.Threading.ParameterizedThreadStart startDelegate = new System.Threading.ParameterizedThreadStart(run);
new System.Threading.Thread(startDelegate).Start("Hello from a thread!");
}
}
The lazy coder’s version without having to instantiate a ParameterizedthreadStart delegate is pretty much the same as the naked version.
public class HelloRunnable
{
public static void run(object message)
{
System.Console.WriteLine(message);
}
public static void Main(string[] args)
{
new System.Threading.Thread(run).Start("Hello from a thread!");
}
}
Something you might have asked. What if I want to return some values in these methods that run in a separate thread? Well, you can’t. The way ThreadStart and ParameterizedThreadStart delegates are designed explicitly prohibit return values. If you indeed want to do that, you have to think differently. In .NET, threading is provided via method calls and there lies the problem. There is no reason why you have to construct a method just to pass a piece of work to be run in a separate thread. In fact, once Lambda expressions are introduced in .NET 3.0 and in particular, the TaskFactory class in .NET 4.0, you can pass in Func<TResult> delegate that returns a value. Saving that, there are workarounds like this.
- At this point, we’re ready to talk about the mysterious second parameter in the constructors, maxStackSize. Each thread has a separate stack and it’s best not to use these overloaded constructors, as per documentation.
The sharp eyed readers will notice a subtle bug in all the above codes. Since the thread scheduler can execute these threads in any order as it sees fit. There is no guarantee that the child thread will complete by the time the parent thread exits. Now, this problem won’t exhibit as the parent thread in this case is the main thread, it exits and the process will exit. In Windows, a process won’t exit until all non-background threads it created exit. In other scenarios, you will have to explicitly wait for all threads to complete unless you’re ok with them being aborted unexpectedly.
You can wait for the child thread to complete in a few different ways.
public class HelloRunnable
{
public static void run(object message)
{
System.Console.WriteLine(message);
}
public static void Main(string[] args)
{
var thread = new System.Threading.Thread(run);
thread.Start("Hello from a thread!");
thread.Join();
}
}
If you prefer not to hold on to the Thread object, ManualResetEvent can come in handy.
public class HelloRunnable
{
private static System.Threading.ManualResetEvent resetEvent = new System.Threading.ManualResetEvent(false);
public static void run(object message)
{
System.Console.WriteLine(message);
resetEvent.Set();
}
public static void Main(string[] args)
{
new System.Threading.Thread(run).Start("Hello from a thread!");
resetEvent.WaitOne();
}
}
It’s possible to use an anonymous delegate to avoid the need for the method definition. Whether this makes sense is debatable.
public class HelloRunnable
{
private static System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent(false);
public static void run(object message)
{
System.Console.WriteLine(message);
resetEvent.Set();
}
public static void Main(string[] args)
{
new System.Threading.Thread(delegate (object message){
System.Console.WriteLine(message);
resetEvent.Set();}).Start("Hello from a thread!");
resetEvent.WaitOne();
}
}
Of course, in the extreme case, you can even use lambda expression like this.
public class HelloRunnable
{
private static System.Threading.AutoResetEvent resetEvent = new System.Threading.AutoResetEvent(false);
public static void run(object message)
{
System.Console.WriteLine(message);
resetEvent.Set();
}
public static void Main(string[] args)
{
new System.Threading.Thread(message => {
System.Console.WriteLine(message);
resetEvent.Set();}).Start("Hello from a thread!");
resetEvent.WaitOne();
}
}