We will now examine a MustInherit
type class, WaitHandle
. WaitHandle
provides a class definition for three other classes, Mutex
, ManualResetEvent
and 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 Mutex
and 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.
The 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 Monitor. Pulse
. 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, Set
and 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 AutoResetEvent
works.
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 DoWork
calls 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 WaitHandle
class. 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 Abort
.
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.
Comments