Click here to Skip to main content
16,008,719 members
Articles / Programming Languages / C#

Using events for thread synchronization

Rate me:
Please Sign up or sign in to vote.
4.74/5 (35 votes)
27 Mar 2002CPOL5 min read 382.6K   2K   67   74
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.

C#
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:-

C#
public void WriteThread()
{
	Thread.Sleep(1000);
	m_x=3;
}	
C#
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);
	}
}
C#
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.

C#
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.

C#
public ManualResetEvent m_mre;

We initialize it in our class's constructor.

C#
m_mre = new ManualResetEvent(false);

Now let's look at out SafeWriteThread function

C#
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.

C#
public void SafeReadThread10()
{
	m_mre.WaitOne();
	ReadThread10();
}
C#
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.

C#
public ManualResetEvent m_mreB;
public ManualResetEvent[] m_mre_array;

Now we add the following initialization code in our constructor

C#
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

C#
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.

C#
public void SafeWriteThread()
{
	m_mre.Reset();
	WriteThread();
	m_mre.Set();
}

As you can see, SafeWriteThread is same as before.

C#
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.

C#
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.

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Nish Nishant is a technology enthusiast from Columbus, Ohio. He has over 20 years of software industry experience in various roles including Chief Technology Officer, Senior Solution Architect, Lead Software Architect, Principal Software Engineer, and Engineering/Architecture Team Leader. Nish is a 14-time recipient of the Microsoft Visual C++ MVP Award.

Nish authored C++/CLI in Action for Manning Publications in 2005, and co-authored Extending MFC Applications with the .NET Framework for Addison Wesley in 2003. In addition, he has over 140 published technology articles on CodeProject.com and another 250+ blog articles on his WordPress blog. Nish is experienced in technology leadership, solution architecture, software architecture, cloud development (AWS and Azure), REST services, software engineering best practices, CI/CD, mentoring, and directing all stages of software development.

Nish's Technology Blog : voidnish.wordpress.com

Comments and Discussions

 
GeneralRe: ReaderWriterLock & Monitor Pin
Blake Coverett30-Mar-02 15:07
Blake Coverett30-Mar-02 15:07 
GeneralRe: ReaderWriterLock & Monitor Pin
Tim Smith30-Mar-02 15:55
Tim Smith30-Mar-02 15:55 
GeneralRe: ReaderWriterLock &amp; Monitor Pin
Nish Nishant30-Mar-02 17:10
sitebuilderNish Nishant30-Mar-02 17:10 
GeneralRe: ReaderWriterLock & Monitor Pin
Tim Smith31-Mar-02 2:34
Tim Smith31-Mar-02 2:34 
GeneralRe: ReaderWriterLock & Monitor Pin
Blake Coverett31-Mar-02 6:49
Blake Coverett31-Mar-02 6:49 
General[Message Deleted] Pin
Nish Nishant30-Mar-02 17:06
sitebuilderNish Nishant30-Mar-02 17:06 
GeneralRe: ReaderWriterLock & Monitor Pin
William E. Kempf1-Apr-02 4:07
William E. Kempf1-Apr-02 4:07 
GeneralRe: ReaderWriterLock & Monitor Pin
Tim Smith30-Mar-02 13:50
Tim Smith30-Mar-02 13:50 
(Note: This might read like a rant, but I am not ranting at you...)

So people like William don't jump down my throat, let me start out by saying that reader/writer locks and monitor objects are great. Back in the 80s, my brother and I added a reader/writer lock to the VMS operating system. (Ah, the good old days.)

Now this probably is much more true for WIN32 than Net, but there are a LOT of things events (or actually, WIN32 objects) do that the CV doesn't do since it isn't part of WIN32. If they were, then this point wouldn't be valid.

In most cases when I implement worker threads, I am having to do much more than just waiting on some condition. To deal with problematic COM objects, I also have to support a message loop in that thread. I also have periodic timers going off to do work. Also, the thread usually has one of those evil events to signal when the thread should be terminated.

Now, sure I could spawn another thread for the timer, but why use the extra resources? I am sure I could overload the event queue to also handle termination.

But Meyers had a very important point with regard to trying to force solutions. The goal is always about trying to introduce the least amount of bugs into your code. If you are having shoehorn a solution to fit a monitor object then you are probably running the risk of producing more unreliable code than if you tried different solution which might historically be more problematic but given the problem at hand, it is the perfect and natural solution.

Since all Nish wanted to know was when the thread was ready, the event in this case was a perfect solution. That is exactly where it was intended to be used. Telling a worker thread he needs to terminate is also a natural use for events. Why use a monitor object which consumes 4 times the system resources when the event works perfectly. (If someone mentions "lost wake" bug, tell them to re-read the documentation on ::SetEvent.)

Mike Myers had a great quote in his interview on the Actor's Studio T.V. program. It was also one of the best episodes of that program. He basically said "There isn't just one right way to do something. Anybody who tells you differently is an a**hole."

Our job as programmers is to gather a collection of solutions for problems. We must understand the risks and benefits of these solutions. This is something Scott Meyers understands very well when he talks about pointers and STL algorithms. When we are faced with a problem, we must be able to decide the proper solution to the problem. The hard part is deciding what that solution might be. That part is more complex that just picking the solution that works the best for most problems.

I actually owe a debt to William and others. During the last month I have learned about monitor objects and have rediscovered reader/writer locks. Ultimately William could prove me wrong and I really wouldn't care. For if I am wrong and William proves this, then I will have learned something and that far out ways any personal shame that I might feel for putting my skills on the line just to be shown wrong.

But what I am fighting for is something much more important to me. The idea that events are being demonized and people blindly steered away from them scares me. Not because I think events should be used by everyone. It is because I see no point in ignorance. Events are just another tool available to programmers. Ignorance of them and how they work serves nobody. I would trade 1000 programmers who are totally ignorant of events for just one who understands them but doesn't use them because a monitor object does exactly what he needs. After all, it is totally illogical to use events when a monitor object does exactly what is needed. But it is also illogical to use a monitor object when an event is exactly what is needed. The sad part is, that the programmer who is ignorant of events will never know.

Me, I have been given the best of both worlds now. I understand events and have now learned about monitor objects. Given that events are more flexible with the WIN32 wait API, sometimes events will be better and sometimes monitors will be better. The important point is that now I have the choice that someone who is ignorant of events or monitor objects will never have.

Tim Smith

I know what you're thinking punk, you're thinking did he spell check this document? Well, to tell you the truth I kinda forgot myself in all this excitement. But being this here's CodeProject, the most powerful forums in the world and would blow your head clean off, you've got to ask yourself one question, Do I feel lucky? Well do ya punk?
GeneralRe: ReaderWriterLock & Monitor Pin
Blake Coverett30-Mar-02 14:39
Blake Coverett30-Mar-02 14:39 
GeneralRe: ReaderWriterLock & Monitor Pin
Tim Smith30-Mar-02 16:01
Tim Smith30-Mar-02 16:01 
GeneralRe: ReaderWriterLock & Monitor Pin
William E. Kempf1-Apr-02 4:24
William E. Kempf1-Apr-02 4:24 
GeneralRe: ReaderWriterLock & Monitor Pin
Tim Smith1-Apr-02 11:04
Tim Smith1-Apr-02 11:04 
GeneralRe: ReaderWriterLock & Monitor Pin
Joao Vaz1-Apr-02 11:24
Joao Vaz1-Apr-02 11:24 
GeneralRe: ReaderWriterLock & Monitor Pin
Tim Smith1-Apr-02 11:59
Tim Smith1-Apr-02 11:59 
GeneralRe: ReaderWriterLock & Monitor Pin
Tim Smith1-Apr-02 12:01
Tim Smith1-Apr-02 12:01 
GeneralRe: ReaderWriterLock & Monitor Pin
Joao Vaz1-Apr-02 22:08
Joao Vaz1-Apr-02 22:08 
GeneralRe: ReaderWriterLock & Monitor Pin
William E. Kempf1-Apr-02 12:18
William E. Kempf1-Apr-02 12:18 
GeneralRe: ReaderWriterLock & Monitor Pin
Tim Smith1-Apr-02 12:40
Tim Smith1-Apr-02 12:40 
GeneralRe: I am sorry... Pin
Tim Smith1-Apr-02 12:31
Tim Smith1-Apr-02 12:31 
GeneralRe: I am sorry... Pin
William E. Kempf1-Apr-02 12:35
William E. Kempf1-Apr-02 12:35 
GeneralRe: I am sorry... Pin
Joao Vaz1-Apr-02 22:33
Joao Vaz1-Apr-02 22:33 
GeneralRe: I am sorry... Pin
Tim Smith2-Apr-02 2:05
Tim Smith2-Apr-02 2:05 
GeneralRe: I am sorry... Pin
Joao Vaz2-Apr-02 2:46
Joao Vaz2-Apr-02 2:46 
GeneralRe: I am sorry... Pin
William E. Kempf2-Apr-02 8:19
William E. Kempf2-Apr-02 8:19 
GeneralRe: I am sorry... Pin
Tim Smith2-Apr-02 9:53
Tim Smith2-Apr-02 9:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.