Using events for thread synchronization






4.74/5 (32 votes)
An introduction to using signaled events for thread synchronization in .NET
Introduction
Whenever you have multiple threads in your program, which is just about always, and whenever those multiple threads might have to access the same resource, which is also a very probable contingency, you will need to incorporate some kind of thread synchronization technique. There are threads that will only access a resource in a read-only manner. Let's call them ReadThreads
and then there are threads that will write to a resource. We call them WriteThreads
or at least let's call them that for now. If a thread reads and writes a shared resource it will still be a WriteThread
. The sample application is a windows forms application created using C# and which has three buttons, one for each case. You need to try out each button to see what happens in each of the cases we discuss below.
Case 1 - No synchronization
Alright let's imagine a situation where we have two ReadThreads
that run in parallel and also access a shared object. In addition there is also a WriteThread
that starts before the ReadThreads
and which sets a valid value for the shared object. I have used Thread.Sleep
to simulate processing time in the sample code snippets below.
Thread t0 = new Thread(new ThreadStart(WriteThread));
Thread t1 = new Thread(new ThreadStart(ReadThread10));
Thread t2 = new Thread(new ThreadStart(ReadThread20));
t0.IsBackground=true;
t1.IsBackground=true;
t2.IsBackground=true;
t0.Start();
t1.Start();
t2.Start();
As you can see, we have started the WriteThread
and then immediately started our two ReadThreads
. I'll show the code snippets for these functions below:-
public void WriteThread()
{
Thread.Sleep(1000);
m_x=3;
}
public void ReadThread10()
{
int a = 10;
for(int y=0;y<5;y++)
{
string s = "ReadThread10";
s = s + " # multiplier= ";
s = s + Convert.ToString(a) + " # ";
s = s + a * m_x;
listBox1.Items.Add(s);
Thread.Sleep(1000);
}
}
public void ReadThread20()
{
int a = 20;
for(int y=0;y<5;y++)
{
string s = "ReadThread20";
s = s + " # multiplier= ";
s = s + Convert.ToString(a) + " # ";
s = s + a * m_x;
listBox1.Items.Add(s);
Thread.Sleep(1000);
}
}
When we run the program, we get the output shown below :-
Aha! So we have got the first two values wrong, one from each thread. What happened was that the ReadThreads started executing before the WriteThread
had finished it's job. This is a totally unwanted situation and we should surely try and do something to avoid this.
Case 2 - Synchronization [One WriteThread - Many ReadThreads]
Now we are going to solve the problem we faced in Case 1. We'll use the ManualResetEvent
thread synchronization class. As before we start the WriteThread
and the two ReadThreads
. The only difference is that we use safe versions of these threads.
Thread t0 = new Thread(new ThreadStart(SafeWriteThread));
Thread t1 = new Thread(new ThreadStart(SafeReadThread10));
Thread t2 = new Thread(new ThreadStart(SafeReadThread20));
t0.IsBackground=true;
t1.IsBackground=true;
t2.IsBackground=true;
t0.Start();
t1.Start();
t2.Start();
We also add a ManualResetEvent
object to our class.
public ManualResetEvent m_mre;
We initialize it in our class's constructor.
m_mre = new ManualResetEvent(false);
Now let's look at out SafeWriteThread
function
public void SafeWriteThread()
{
m_mre.Reset();
WriteThread();
m_mre.Set();
}
The Reset
function sets the state of the event object to non-signaled. This means the event is currently not set. Then we call the original WriteThread
function. Actually we could have skipped the Reset
step because we had set the state to non-signaled in the ManualResetEvent
constructor earlier. Once the WriteThread
function returns we call the Set
function which will set the state of the event object to signaled. Now the event is said to be set.
Now, let's take a look at out two SafeReadThread
functions.
public void SafeReadThread10()
{
m_mre.WaitOne();
ReadThread10();
}
public void SafeReadThread20()
{
m_mre.WaitOne();
ReadThread20();
}
The WaitOne
function will block till the event object's signaled state is set. Thus in our particular scenario, both the SafeReadThreads
will block till the event object is signaled. Our SafeWriteThread
will set the event only after it has done it's job. Thus we ensure that the reading threads start reading the shared resource only after the writing thread has done it's job. Now when we run the program we get this output which is what we wanted to get.
Voila! Perfecto!
Case 3 - Synchronization [Many WriteThreads - Many ReadThreads]
Now assume we have a situation where we have two WriteThreads
. Now the ReadThreads
would have to wait till all the WriteThreads
have finished their work. In a real scenario, both the WriteThreads
would probably be running together, but in our example I've run them in a serialized order where the the second WriteThread
starts only after the first one has finished. This is only for ease of simulation. In our case since the second WriteThread starts only after the first WriteThread
the ReadThreads
need to only wait on the second thread, but as I already said, simply imagine that the two WriteThreads
are running in parallel.
We add another ManualResetEvent
object for the second thread and also an array of ManualResetEvent
objects.
public ManualResetEvent m_mreB;
public ManualResetEvent[] m_mre_array;
Now we add the following initialization code in our constructor
m_mreB = new ManualResetEvent(false);
m_mre_array = new ManualResetEvent[2];
m_mre_array[0]=m_mre;
m_mre_array[1]=m_mreB;
Now lets see how we start the four threads
Thread t0 = new Thread(new ThreadStart(SafeWriteThread));
Thread t0B = new Thread(new ThreadStart(SafeWriteThreadB));
Thread t1 = new Thread(new ThreadStart(SafeReadThread10B));
Thread t2 = new Thread(new ThreadStart(SafeReadThread20B));
t0.IsBackground=true;
t0B.IsBackground=true;
t1.IsBackground=true;
t2.IsBackground=true;
t0.Start();
t0B.Start();
t1.Start();
t2.Start();
As you can see, we now have two StartThreads
and two WriteThreads
. Lets see their implementations.
public void SafeWriteThread()
{
m_mre.Reset();
WriteThread();
m_mre.Set();
}
As you can see, SafeWriteThread
is same as before.
public void SafeWriteThreadB()
{
m_mreB.Reset();
m_mre.WaitOne();
Thread.Sleep(1000);
m_x+=3;
m_mreB.Set();
}
Well, as you can see we have used another event object for this second WriteThread
. For the sake of simulation there is a wait for the first thread to finish its work, but as mentioned before this is not a true representation of the real life state of affairs.
public void SafeReadThread10B()
{
WaitHandle.WaitAll(m_mre_array);
ReadThread10();
}
public void SafeReadThread20B()
{
WaitHandle.WaitAll(m_mre_array);
ReadThread20();
}
As you can see we have used a function called WaitAll
. It's a static member function of the WaitHandle
class which is the base class for our ManualResetEvent
class. The function takes in an array of WaitHandle
objects to which we pass our ManualResetEvent
object array. The casting is implicitly done as we are casting to a parent class. What WaitHandle
does is this. It will block till each object in the array has been put into a signaled state or in other words till every object in the array has been set. When we run the program this is what we get.
Cool, huh? It worked nice and fine.
AutoResetEvent
There is a very similar class called AutoResetEvent
. The difference from the ManualResetEvent
class is that the AutoResetEvent
is automatically reset to non-signaled after any waiting thread has been released. The best I could figure out for the purpose of this class is this. Lets assume we have several threads waiting for access to an object. We don't want all of them to get access together. So when we are ready to allow access to one thread, we set the event object they are all waiting on. This object will be an AutoResetEvent
object. Now one of the threads is released, but the moment that happens, the event is non-signaled automatically. Thus the other threads will continue to wait till the main thread or the thread that is accessing the event object decides to set the event to a signaled state.
I have put together a simple console application to demonstrate this class.
class Class1
{
AutoResetEvent m_are;
static void Main(string[] args)
{
Class1 class1 = new Class1();
}
Class1()
{
m_are = new AutoResetEvent (false);
Thread t1 = new Thread(new ThreadStart(abc));
Thread t2 = new Thread(new ThreadStart(xyz));
t1.Start();
t2.Start();
m_are.Set();
Thread.Sleep(3000);
m_are.Set();
}
void abc()
{
m_are.WaitOne();
for(int i=0;i<5;i++)
{
Thread.Sleep(1000);
Console.WriteLine("abc abc abc");
}
}
void xyz()
{
m_are.WaitOne();
for(int i=0;i<5;i++)
{
Thread.Sleep(1000);
Console.WriteLine("xyz xyz xyz");
}
}
}
When we run the above program we get something like this as output.
abc abc abc abc abc abc abc abc abc xyz xyz xyz abc abc abc abc abc abc xyz xyz xyz xyz xyz xyz xyz xyz xyz xyz xyz xyz
Conclusion
This essay is not a comprehensive one in the sense it does not detail each and every little nuance associated with the thread synchronization event classes. But I do hope it gives you a start from where you can reach out to taller heights or perhaps the expression should be reach down to even more profound depths.