We will now examine a
MustInherit type class,
WaitHandle provides a class definition for three other classes,
AutoResetEvent, and provides means for your own objects to inherit synchronization functionality. These objects allow threads to wait until classes derived from
WaitHandle are signaled. The
WaitHandle derived classes add functionality over Monitor in that threads can be programmed to wait until multiple classes are signaled. Of course, along with more power and flexibility comes more work and chance of problems.
The two reset event classes can be used in context with
Mutex to provide similar functionality to
Monitor. The major difference between
Monitor is that
Mutex can be used across processes. You can think of the two reset event classes as being switches. The thread cannot enter a
Mutex unless its object is signaled. We will examine them in detail next.
AutoResetEvent class can be compared to the
Monitor. Pulse method. Imagine it as a tollbooth. Each car has to pay to go through, the signal, and then the gate closes behind the car when it passes making the next car in line pay again. The
AutoResetEvent class is like this. It automatically goes back to unsignaled after being signaled and a thread goes through, just like
ManualResetEvent can be described as a water hose, once open it lets everything through until you close it yourself.
Let’s examine the
AutoResetEvent in detail first. It comes equipped with two methods to control its state,
Reset. Set allows one thread to acquire the lock on the object. After allowing a thread to pass through,
Reset will automatically be called, returning the state to unsignaled.
On the first call to
Set the runtime will make sure that the state of the object is signaled. Multiple calls to
Set have no effect if the state is already signaled, and it will still allow only one thread to pass. You do not know the order of threads for each signal either. If multiple threads are waiting on an object, you are only guaranteed that one will get in per
Set when a wait method is called.
Reset can be used to change the state of the object back to unsignaled from signaled before a thread calls a wait method on the object.
Reset will return
True if it can change the state back to unsignaled or
False if it can not. It has no effect on an unsignaled object. The code below will show how an
Dim WaitEvent As AutoResetEvent WaitEvent = New AutoResetEvent(False) Public Sub DoWork() 'do some long processing task simulate by sleeping Thread.Sleep(5000) WaitEvent.Set() End Sub Public Sub Thread2() 'we want thread 2 to run after thread1 is 'finished. It will take 'the data computed by thread 1 and do 'something to it WaitEvent.WaitOne() 'Wait until DoWork is done and the 'WaitEvent is signaled 'wait until thread 1 is done to keep going End Sub
In the above code, we make a new instance of an
AutoResetEvent. Our main thread then would call
DoWork while a secondary thread would call
Thread2. When the secondary thread reached the
WaitOne call, it would enter the
WaitSleepJoin state until the main thread calls the
Set method after its long processing task allowing
Thread2 to continue execution. When
WaitEvent.Set() it signals that it is available for another thread that is waiting to obtain continue running. Since our
Thread2 is waiting, it continues now.
To fully understand the
AutoResetEvent class, we must also examine the
AutoResetEvent is derived from
WaitHandle. It inherits several methods at which we will look at.
The first method,
WaitOne, we have already seen in action in the above code sample. Basically, it will wait until the object has become signaled.
WaitOne without any parameters will wait infinitely until the object becomes signaled. There are also several overrides that allow you to wait for an amount of time, both in milliseconds or a
TimeSpan. If time elapses on these methods,
WaitOne will return false indicating that a lock couldn’t be obtained.
The timed methods of
WaitOne also take a boolean parameter that is worthy of note. If you pass false to the parameter, nothing different happens from calling the standard no parameter
WaitOne except for the timeout. If true is passed, and
WaitOne is called from a COM+ synchronized context, it will force the thread to exit the context before waiting. This method won’t affect your code unless you use the COM+ methods of synchronization, which we will discuss later.
The next method,
WaitAll, is very useful when you have a large amount of work to accomplish and want to use multiple threads to accomplish it. This allows a thread to wait on multiple objects. Once all objects in the array are signaled the waiting thread is allowed to continue execution.
As with the
WaitOne method, the no parameter method waits indefinitely while two other methods exist to wait for a specific amount of time. The method also has the boolean parameter for exiting a synchronized context. Be careful when waiting infinitely when using
WaitAll. If you don’t signal all instances of the
AutoResetEvent correctly as shown below, your waiting thread will never resume.
Lets take a look at a code example of how to use
WaitAll. First the form’s code:
Dim WaitAllEvents(1) As AutoResetEvent Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click Dim thread1 As Thread Dim thread2 As Thread 'first we create 2 threads as assign them to subs thread1 = New Thread(AddressOf Thread1Work) thread2 = New Thread(AddressOf Thread2Work) 'Next our 2 AutoRresetEvent instances are created WaitAllEvents(0) = New AutoResetEvent(False) WaitAllEvents(1) = New AutoResetEvent(False) thread1.Start() thread2.Start() 'after starting the threads we tell the main thread to 'wait until all instances of AutoResetEvent have become 'signaled with a call to Set() WaitHandle.WaitAll(WaitAllEvents) Console.WriteLine("All threads done exiting main thread") thread2 = Nothing thread1 = Nothing End Sub Private Sub Thread1Work() Thread.Sleep(5000) Console.WriteLine("Thread1 done") WaitAllEvents(0).Set() 'I’m done so signal my Event End Sub Private Sub Thread2Work() Thread.Sleep(3000) Console.WriteLine("Thread2 done") WaitAllEvents(1).Set() 'I’m done so signal my Event End Sub
Now some code in a Module.
<MtaThread()> Public Sub Main() Dim frm As Form1 frm = New Form1() frm.ShowDialog() End Sub
The output from the code is:
Thread2 Done Thread1 Done All threads done exiting main thread
As you can see from the output the main thread waits until all objects in its
WaitAllEvents array are signaled. Another item that is worthy to note here is the attribute
<MTATHREAD()>. This signifies that the main thread should run as a multithreaded apartment style thread and not as a single threaded apartment, which is the default.
WaitAll must be called from a thread that is an
MTAThread. If not it will throw a
NotSupportedException. While done as an example above with a simple WinForm, you should not run your main thread that opens Window’s Forms on an
MTAThread. This will cause some problems with some of the controls.
The single threaded apartment style thread model guarantees that only one thread is accessing code at one time. In order for Windows Forms projects to work correctly, they must be run in a single threaded apartment. This does not mean than worker threads cannot be created and used. We will go into more detail about Windows Form synchronization later in the case study. Some of the other project types, such as the Window’s service project, are by default multithreaded apartments. The MTA style will also be discussed later. In these situations,
WaitAll can be used very effectively.
The last method we will examine is
WaitAny. This method waits until any one object in the array is signaled. An example of its use could be a dictionary search engine. The program could start two threads, the first that started with the letter A and the second that started with the letter Z. The first match found by either thread will terminate the others that are searching and return control to the main application. The return of this method tells you the position of the array that was signaled. Like the other two methods, you can wait indefinitely or for a specific amount of time.
Let’s look at a code example.
Dim WaitAnyEvents(1) As AutoResetEvent Private Sub Start_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click Dim Thread1 As Thread Dim Thread2 As Thread Thread1 = New Thread(AddressOf Thread1Work) Thread2 = New Thread(AddressOf Thread2Work) WaitAnyEvents(0) = New AutoResetEvent(False) WaitAnyEvents(1) = New AutoResetEvent(False) Thread1.Start() Thread2.Start() WaitHandle.WaitAny(WaitAnyEvents) Console.WriteLine("One thread done exiting main thread") End Sub Private Sub Thread1Work() Thread.Sleep(5000) Console.WriteLine("Thread1 done") WaitAnyEvents(0).Set() End Sub Private Sub Thread2Work() Thread.Sleep(3000) Console.WriteLine("Thread2 done") WaitAnyEvents(1).Set() End Sub
In examining the above code, we see that an array of
AutoResetEvent has been created as a form level variable so that all subroutines can access it. We have put a command button on the form. This button is the main worker of the example. When it is clicked, we create two new threads and assign their individual subs to run upon starting. The subs simulate work by sleeping for a while. When done sleeping, a string is out put to the debug window and the corresponding
AutoResetEvent is signaled. This causes the main thread to resume running. You should receive the following output from the example:
Thread2 Done One thread done exiting main thread Thread1 done
The output shows that the main thread resumes running after the first object has been released. Because the main thread doesn’t abort the first thread,
Thread1, it eventually finishes outputting its string “Thread1 done”. If the other threads are no longer needed they should be aborted manually from your main thread with a call to
Now let’s examine a way to signal an event and have it stay signaled, the
ManualResetEvent. This event will stay signaled no matter how many threads do a wait method on it. This only way to change the state is to call
Reset. You can use the object to control access to data that multiple threads are waiting on. For example, we might have two threads or more, we might not know (or care), waiting on a piece of data that another thread is calculating. When this thread gets done with its work, we can let all other threads in to access the data. At some later time if we determine that the data needs to be recalculated, we can turn off the threads from accessing it. Then do our new calculations.
Let’s look at some code now.
Private ManualWaitEvent As ManualResetEvent Dim Thread1 As Thread Dim sData As String Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System. EventArgs) Handles MyBase.Load ManualWaitEvent = New ManualResetEvent(False) Thread1 = New Thread(AddressOf ReadWork) Thread1.IsBackground = True Thread1.Start() End Sub Private Sub ReadWork() 'this method will wait until ManualWaitEvent is 'signaled Dim i As Integer For i = 0 To 100 ManualWaitEvent.WaitOne() Console.WriteLine(sData & i.ToString()) Thread.Sleep(1000) Next 'i End Sub Private Sub btnSet_Click(ByVal sender As System.Object, ByVal _
e As System.EventArgs) Handles btnSet.Click sData = "Work Done: " ManualWaitEvent.Set() End Sub Private Sub btnReset_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnReset.Click ManualWaitEvent.Reset() End Sub
When the form loads, we create a new instance of a
ManualResetEvent in the unsignaled state. A thread is created and started. The thread then waits until the event becomes signaled. When signaled it reads a string that we are using to represent our data. This is a very powerful method of controlling synchronization when you have multiple threads. It lets you fine tune access to variables easily. You can easily switch on and off access to the data.
Every second, the thread will output “Work Done: “ and the value of
i until the
ManualWaitEvent is unsignaled by pressing the reset button. If the set button is pressed again the thread will resume its work and continue to output data to the output window. Every time
ManualWaitEvent. WaitOne() is called, a check of the state of
ManualWaitEvent is done. If this call were outside of the loop, all one hundred values of
i would have been printed the first time the set button was pressed.
Also note the
IsBackground call in the form load event. This makes
Thread1 a child thread to the main process thread. If the main thread is terminated, the operating system will also terminate any background threads related to the main one. If the thread were not a background thread, it would continue running until it was finished, even when we closed our main thread out. If the state of
ManualWaitEvent were unsignaled, the thread would be waiting on an object that could never be signaled again since our main form was gone. This results in the process being left in memory. This should be avoided by making all threads background threads, unless it is 100% necessary for the thread to finish regardless of the state of the application. Make sure that these non-background threads have access to any resources they need also. If termination of the main running program disposes of a needed resource, the thread will never finish or result in an error.