Introduction
One of my New Year wishes for this coming year was that the standard committees
would agree on threading classes for the C++ language. This limitation of the
C++ language standard means that I have to rewrite my threading library each
time I start a new job with a new company. I've always wished there was a standard
threading library that I could use wherever I go. Fortunately, C# does not have
this disadvantage. Right from the get go, the language inherits an entire set
of threading classes from the .NET framework. The "System.Threading
"
.NET namespaces includes 14 utility classes, 4 exception classes, 2 structures,
6 delegates and 3 enumerations. I'll present most of these in this article.
Creating Threads
Creating a thread in C# is close to trivial, but not quite. The only non-trivial thing about creating a thread is .NET delegate-classes. Let me explain in few words what is a delegate class. The delegate is a wrapper around a code construct in the .NET. The code construct could be an object instance, an instance method or a static method.
Delegates are used when you want to pass one of the three code constructs as
a parameter to another method. When creating a new thread you have to use the
ThreadStart
delegate class to wrap the instance method that will
be executed in the newly created thread. The instance method must return void
and must not have any parameters.
void ThreadStart()
To create a new thread, first create a new ThreadStart object, passing the instance method of the thread procedure in the constructor. The new delegate object is then passed to the constructor of the Thread.
Thread thread = new Thread(
new ThreadStart(obj.ThreadStart));
You've now created a new thread, but the thread is not yet started. To start
the thread, you call the Thread.Start
instance method.
thread.Start();
And that's it. You have a new running thread. A complete console application that creates a thread and outputs a couple messages to the console window is shown below.
using System;
using System.Threading;
namespace ConsoleApplication1
{
class Class1
{
static void PrintHelloFromThreadName()
{
Console.WriteLine("Hello, from thread
{0}",
Thread.CurrentThread.Name);
// {0}
}
public void ThreadStart()
{
PrintHelloFromThreadName();
}
static void Main(string[] args)
{
Thread.CurrentThread.Name = "Main
thread";
Class1 obj = new Class1();
Thread thread = new Thread(
new ThreadStart(obj.ThreadStart));
thread.Name = "Forked thread";
thread.Start();
PrintHelloFromThreadName();
}
}
}
A nice feature of .NET threads, and for that matter any .NET object, is
the ability name the object. If you name your threads, then the debugger will
pick up those names and you'll have a much easier time debugging (see Figure
1). The frame in the bottom left of the IDE window in Figure 1 shows all the
threads in out C# application. I set a breakpoint in the PrintHelloFromThread
Name static method in Listing 1 and ran the application. When the application
stops on the breakpoint, I called up the threads window from the menu bar, Debug
| Window | Threads
. As you can see, the Name in the threads window of
the IDE is the same as the name given the Thread object in our C# code.
Thread Pools
I was very impressed when I found out that the .NET framework library included
the "System.Threading.ThreadPool
" class. I was also impressed
by how easy it was to use. You need not create the pool of threads, nor do you
have to specify how many consuming threads you require in the pool. The ThreadPool
class handles the creation of new threads and the distribution of the wares
to consume amongst those threads.
You can kick off a consuming thread pool by simply invoking the ThreadPool
.
QueueUserWorkItem
static method.
ThreadPool.QueueUserWorkItem( new WaitCallback( Consume), ware);
The parameters of the QueueUserWorkItem
static method are the
WaitCallback
delegate that wraps the instance method used in consuming
your ware and the ware that you are passing to the method. Your consuming instance
method must return void and take one object parameter. The ware that is passed
to the QueueUserWorkItem
method will be passed into your consuming
instance method as the one object parameter. (For the rest of this article I
define a ware to be an item that is produced by the producing thread and consumed
by a consuming thread in the consumer-producer design pattern. This is a very
narrow definition of the word, but one that suits this article. )
public void Consume(Object obj)
Again, the simplicity of C# and the .NET framework shine through. In just a few lines of code, I've recreated a multithreaded consumer-producer application (see below).
using System;
using System.Threading;
using System.Diagnostics;
namespace ConsoleApplication2
{
public class Ware
{
public int id;
public Ware(int _id)
{
id = _id;
}
}
class Class1
{
public int QueueLength;
public Class1()
{
QueueLength = 0;
}
public void Produce(Ware ware)
{
ThreadPool.QueueUserWorkItem(
new WaitCallback(Consume),
ware);
QueueLength++;
}
public void Consume(Object obj)
{
Console.WriteLine("Thread {0} consumes
{1}",
Thread.CurrentThread.GetHashCode(),
//{0}
((Ware) obj).id); //{1}
Thread.Sleep(100);
QueueLength--;
}
public static void Main(String[] args)
{
Class1 obj = new Class1();
for (int i = 0; i < 1000; i++)
{
obj.Produce(new Ware(i));
}
Console.WriteLine("Thread {0}",
Thread.CurrentThread.GetHashCode()
); //{0}
while (obj.QueueLength != 0)
{
Thread.Sleep(1000);
}
}
}
}
I added the line Thread.Sleep(100)
in the Consume
method to simulate the processing that a consumer would normally have performed
on the ware. If I didn't include this Sleep'ing, then one consumer thread could
have handled all 100 wares. The additional Sleep'ing forces the .NET framework
to create additional threads and more accurately portrays the features of the
ThreadPool
class.
Synchronization Objects
The previous code contains some rather inefficient coding when the main thread
cleans up. I repeatedly test the queue length every second until the queue length
reaches zero. This may mean that the process will continue executing for up
to a full second after the queues are finally drained. Wow! I can't have that.
OK! Maybe that's not a good reason to change the code, but it is a convenient
excuse for me to introduce you to the System.Threading.ManualResetEvent
class. Using a ManualResetEvent
object, I could trigger
the main thread to complete as soon as the last ware was consumed. I'll do this
by creating two new instance data members, a bool WaitForComplete
to tell us when the main thread is waiting to exit and a ManualResetEvent
Event object that will signal the main thread to exit.
public void Wait()
{
if (QueueLength == 0)
{
return;
}
Event = new ManualResetEvent(false);
WaitForComplete = true;
Event.WaitOne();
}
public void Consume(Object obj)
{
Console.WriteLine("Thread {0} consumes {1}",
Thread.CurrentThread.GetHashCode(), //{0}
((Ware) obj).id); //{1}
Thread.Sleep(100);
QueueLength--;
if (WaitForComplete)
{
if (QueueLength == 0)
{
Event.Set();
}
};
}
When the consuming thread finishes consuming a ware and detects that the WaitForComplete
is true, it will trigger the Event when the queue length is zero. Instead of
calling the while block when it wants to exit, the main thread calls the Wait
instance method. This method sets the WaitForComplete
flag and
waits on the Event object.
Race Condition
Let me test your threading prowess. The previous listing contained a race condition. Can you find it? Take a minute or two before continuing. Tic! Tic! Tic!
A race condition is a bug caused by an incorrect assumption as to the timing
of two events, that is, that one event would always occur before the other.
The race condition occurs when the system shuts down. If the main thread is
swapped out in the Wait
instance method between testing if the
queue length is zero and setting the WaitForComplete
flag to true
and then the last consuming thread exits the Consume
instance method
while the main thread is in this state, the event will never be triggered. I
ran the code a few hundred times and was never able to trigger the condition.
You can't reproduce it because the main thread should be waiting on the event
object well before the last consuming-thread exits.
Monitor and Lock
I could have arranged the code otherwise to prevent this race condition, but now I've created another opportunity to introduce you to the System. Threading. Monitor class and the lock C# construct. The monitor design pattern is most familiar to Java developers. In Java, the synchronized keyword allowed the developer to create quick critical sections within their code. The Java construct was often called a monitor.
The .NET framework presents a similar class called the Monitor that implements
traditional wait and signal methods called Wait
and Pulse
.
The C# compiler uses this Monitor
class to implement a language
construct called a lock. The lock is established on an object and while the
lock is established, nobody else can acquire the lock and must wait till the
lock is freed. I used this lock construct to prevent our previous race condition
(see Listing 4).
public void Wait()
{
lock (this)
{
if (QueueLength == 0)
{
return;
}
Event = new ManualResetEvent(false);
WaitForComplete = true;
}
Event.WaitOne();
}
public void Consume(Object obj)
{
Console.WriteLine("Thread {0} consumes {1}",
Thread.CurrentThread.GetHashCode(), //{0}
((Ware) obj).id); //{1}
Thread.Sleep(100);
lock (this)
{
QueueLength--;
if (! WaitForComplete)
{
return;
}
}
if (QueueLength == 0)
{
Event.Set();
};
}
Preventing the concurrent setting and testing of the queue length and WaitForComplete
flag by two different threads removes the race condition. The lock ensures that
the setting and testing of these two variables is essentially atomic.
Join
Before .NET, I was often asked questions about how to wait for a Win32 thread
to exit. The solution was to acquire a handle to the thread and wait on the
handle. Or alternatively, you could setup an event that was triggered at the
end of the thread and wait on that event. .NET provides us with a simpler
method of doing the same. If you call the Thread.Join
instance
method, then the current thread will wait until the thread represented by the
Thread
object is terminated.
using System;
using System.Threading;
using System.Diagnostics;
namespace ConsoleApplication7
{
class Class1
{
public void Pump()
{
for (int i= 0; i< 100; i++)
{
Console.WriteLine("Value
{0}", i);
Thread.Sleep(1);
}
}
static void Main(string[] args)
{
Class1 obj = new Class1();
Thread pump = new Thread(
new ThreadStart(obj.Pump));
pump.Start();
Thread.Sleep(500); // force the other
thread
// thru a couple iterations
pump.Join(); // wait until the thread
is
// completed
Console.WriteLine("Goodbye");
}
}
}
In this previous listing, the main thread creates a new thread (pump), then
waits for the thread to complete by calling the pump.Join
instance
method. If you run the previous code, as is, then the output will be the numbers
0 to 99 and finally the word Goodbye. If you remove the call to pump.Join
,
then the Goodbye message may be printed before the last number. I chose to put
the main thread to sleep for half a second as this displayed the Goodbye message
in the middle of the stream of numbers (when pump.Join
was removed).
Timer
Early in the article, I introduced you to the ManualResetEvent
class. This class allowed you to set and reset (signal and unsignal) the event
by calling the Set
and Reset
instance methods. The
System.Threading.AutoResetEvent
class is very similar to the ManualResetEvent
class, but when a thread waiting on the event is signaled, the one thread is
released and the event is returned to the unsignaled state. This removes the
necessity to reset the signal after a thread is signaled.
Another great class in the System.Threading
namespace is the Timer
class. This class allows you to signal an event at a particular interval in
time in the future. The Timer
class is implemented using a delegate
callback instance method. When the Timer
is signaled, the class
calls the instance method that you specified in the constructor of the Timer
object. The Timer
callback can also receive a parameter object
passed in the call to the Timer
constructor. Presented below is
a small sample using the AutoResetEvent
and Timer
classes.
using System;
using System.Threading;
namespace ConsoleApplication8
{
class Class1
{
public void TimerCallback(Object obj)
{
Console.WriteLine("Timer triggered");
((AutoResetEvent) obj).Set();
Thread.Sleep(1000);
((AutoResetEvent) obj).Set();
}
static void Main(string[] args)
{
Class1 obj = new Class1();
AutoResetEvent ev =
new AutoResetEvent(false);
Timer timer = new Timer(
new TimerCallback(obj.TimerCallback),
ev, 1000, 0);
ev.WaitOne();
Console.WriteLine("Event Fired");
ev.WaitOne();
Console.WriteLine("Event Fired");
}
}
}
Note that the Timer
callback instance method is wrapped in a TimerCallback
delegate object. The main thread will create an AutoResetEvent
object and a Timer
object. The main thread then waits on the event
object. The TimerCallback
instance method is called after one second,
triggering the event object. Because the event object is automatically reset,
when the main thread attempts to wait on the event again, the thread yields
until the event is signaled a second time. The TimerCallback
instance
method waits another second and then signals the event a second time, releasing
the main thread.
More
In the second part of this article, I will complete my discussion of the synchronization objects and will discuss thread local storage, COM interoperability and thread states.
Comments