Introduction
Multithreading, a very powerful technique, is essential for
modern software development. Software users expect to work with a responsive
program that they do not have to wait on, which is a very reasonable demand
with the processor speeds that are currently available. Enter multithreading.
Multithreading is the concept of having several different paths of code running
at the same time.
When you introduce multithreading in your applications, you
immediately make programming more complicated and add design time. You must
know exactly what your application and all its threads are doing at all times.
You have to account for deadlocks, race conditions and corrupting variable
values. In this article, we will examine the different methods in Visual Basic
.NET to accomplish thread synchronization. We will define deadlocks and race
conditions and learn how to avoid these common problems with multithreading.
System Requirements
I will assume that you already have knowledge of basic
threading in Visual Basic .NET. You should know how to create threads and know
how to do basic threading operations like Join and Sleep. A copy of Visual
Studio .NET is required to run the code samples and see the output. The code
was written with Visual Studio.Net using version 1.0 of the .NET Framework with
service pack 2.
Case Study Structure
This case study has three main parts. Multithreading
requires a technique called synchronization to eliminate the problems described
above, so we will first take a brief look at synchronization. Then an in-depth
look at all methods available in Visual Basic .NET for synchronization will be
presented where you will learn how to correctly synchronize a multithreaded
application. After this, a look at Windows Form synchronization and threading
apartment styles will show the differences a programmer must handle between
standard synchronization and visual GUI synchronization.
Synchronization
What is thread synchronization? Imagine the following lines
of code:
Listing 1
Dim X as Integer
X = 1
X = X + 1
The line X = X + 1 is a single operation to a programmer.
But consider this line from a computer’s perspective. Computers use machine
language, which could mean many separate operations for each line of code. For
example, the line above could be broken down into several operations, such as:
Move the value of X into a register, move the value 1 into another register,
add the two registers and place the value into a third register and finally,
move the added values into the memory address of the variable X.
Imagine the above situation with multiple threads trying to
access the variable X at the same time. Synchronization is the process of
eliminating these kinds of errors. Without synchronization programming, the
computer could stop the first thread at any point in time and let the second thread
access the variable. If the second thread was also incrementing X by 1, it
might finish and then the computer would resume the original thread that was running.
This thread would restore its variable information, replacing the new X with
the old value and nullifying the work that the second thread accomplished.
This is called a race condition. These errors are very hard to find and it is
best to put time in preventing them.
To synchronize code, you utilize locks. A lock is a way to
tell the computer that the following group of code should be executed together
as a single operation and not allow other threads to have access to the
resource that is locked until the locking code is finished. In the case study
we will examine the different types of locks and objects that allow locking and
discuss when to use each method. When your code can handle multiple threads
safely, it is considered thread safe. This common term is used on code
libraries and controls to designate that they are compatible with multiple
threads.
Synchronization also adds a new type of bug you need to
watch out for, deadlocking. Deadlocking can occur if you are not careful with
your locking techniques. For example, assume that we have two resources, A and
B. Thread 1 calls and locks resource A at the same time thread 2 calls and
locks resource B. Thread 1 then requests resource B and thread 2 requests
resource A. This is called a deadlock. Thread 1 cannot release resource A
until it gets resource B and thread 2 cannot release resource B until it gets
A. Nothing happens and your system cannot complete either of the two threads.
Needless to say, this is very bad.
The only way to avoid deadlocks is to never allow a
situation that could create one. Code both threads to allocate resources in
the same order. Have thread 1 allocate A and then B and the same with thread
2. This way thread 2 will never start until thread 1 is finished with resource
A. Then it will wait until thread 1 is finished with resource B before
continuing and avoid the deadlock. Another good practice is to lock resources
as late as possible. Try to avoid getting locks until you absolutely need them
and then release them as soon as possible. Next, we shall take a look at all
the different methods of thread synchronization that the common language
runtime provides.
Interlocked Class
Because they are very common programming techniques,
variable increment and decrement have their own framework class, the
Interlocked class. This class provides simple thread-safe methods to do some
common tasks with variables. The Increment and Decrement methods add or
subtract 1 from a variable. These methods can be considered “atomic."
This means that the operating system will consider the entire operation as one,
not allowing other threads to interrupt their execution. The class is a member
of System.Threading namespace. To use the functions without fully qualifying
the name, add an Imports System.Threading line. I will assume that the
System.Threading namespace has been imported for all of the examples in the
case study.
Listing 2
Dim X as Integer
X = 1
X = Interlocked.Increment(X)
X = Interlocked.Decrement(X)
The above code ensures that the computer will not interrupt
the increment or decrement of the variable X.
There are two additional methods in the Interlocked class,
Exchange and CompareExchange. Let us take a closer look at the two. The
Exchange method replaces the value of a variable with the value supplied. The
second value could be a hard coded value or a variable. Do not let the name of
the method, Exchange, confuse you though. Only the first variable passed in
the first parameter will be replaced by the second. The method will not really
exchange the values of two variables.
Listing 3
Dim X as Integer = 5
Dim Y as Integer = 1
Interlocked.Exchange(X, Y) ‘X now equals 1 Y isstill 1
Interlocked.Exchange(X, 4) ‘X now equals 4
CompareExchange will do a comparison of two variables and if
they are equal, replace the one used as the first parameter with the supplied
value.
Listing 4
Dim i As Integer
i = 200
Interlocked.CompareExchange(i, DateTime.Now.Day, 200)
The above code creates a new integer and then assigns the
value 200 to it. We then call the Interlocked.CompareExchange. The method
compares the variable i with 200 and since they are the same, it will replace i
with DateTime.Now.Day, the current day of the month.
The Interlocked class allows you to do basic programming
techniques and make them thread safe. Let us examine how to do more than just
basic commands now. The .NET Framework provides several classes and Visual
Basic .NET provides one method to handle complete synchronization. First, we
will take a look at the SyncLock Visual Basic .NET keyword.
SyncLock Keyword
The SyncLock keyword (lock in C#) gives an easy way to
quickly lock parts of code. It is now a built in keyword in Visual Basic.Net.
Take a look at the following code segment:
Listing 5
Dim sText as String
Dim objLock as Object = New Object()
SyncLock objLock
sText = "Hello"
End SyncLock
First we declare a new string, sText. Then we set up a
SyncLock block to control access to the object using another locking object,
objLock. This guarantees that only one thread at a time can set the object to
the string "Hello". A lock object must be used or an exception will
be thrown on the Exit call. If you try to use an object that has changed since
the Enter call, the Exit will fail, so you cannot lock on sText itself. The
most common use of SyncLock is to lock the entire object it is in by using the
Me keyword as the parameter of the SyncLock. This will lock the object for all
threads except the executing one. This provides a very high degree of control
over the locking patterns of the object at the cost of flexibility.
Listing 6
Public Sub Foo()
Dim sText As String
SyncLock Me
sText = "Hello"
End SyncLock
End Sub
Locking the entire object is usually a great waste of time
and processing power. Other methods in the Me object that have locking code
based on the Me object will not be accessible to any threads while in the
lock. If a more flexible approach is needed, a locking variable can be used.
Locks can only be obtained on reference types. If a lock on a value type is
needed, you must use a locking object as shown below. The code locks access to
iData via a reference type, System.Object. Imagine the locking object as a key
to the code. Only one thread at a time can have the key. This allows for much
greater control over what gets locked. This method will also not lock the
whole Me object. Other threads are free to access other methods of Me, which
is much more efficient and will reduce the possibility of deadlocks.
Listing 7
Public Sub Foo()
Dim iData As Integer
Dim objLock As Object = New Object()
SyncLock objLock
iData = 3
End SyncLock
End Sub
One drawback to using SyncLock is that other threads must
wait forever for the lock to be released if they need the locked resource.
They will never time out. If you are not careful and enter an infinite loop in
the locking thread, or hog resources, you can easily create deadlocks or
periods of time where nothing happens. In later sections, better methods of
synchronization will be discussed.
Flow control statements such as GoTo cannot move the code
flow into a SyncLock block of code. The thread must execute the SyncLock
keyword. Old Visual Basic 6 error handling cannot be used from inside a
SyncLock block either since it uses exception handling internally. Since all
new code should be written with exception handling, you probably will not run
into a situation like this unless upgrading a legacy application. I would
highly recommend rewriting any legacy error handling even if the methods are
not used for multithreading. Neither of the following code blocks will
compile:
Listing 8
SyncLock Me
On Error GoTo Errhandle ‘won’t compile
Dim i As Integer
i = 5
End SyncLock
Exit Sub
Errhandle:
Or:
GoTo EnterHere ‘won’t compile
SyncLock Me
EnterHere:
Dim i As Integer
i = 5
End SyncLock
In the next section we will examine how SyncLock works
internally.
Monitor Class
To examine how SyncLock works, we have to explore a
framework class, the Monitor class. The Monitor class does exactly what it
says: monitors the access to a region of code and prevents multiple threads
from entering. If you are familiar with win32 programming using C++, Monitor
is similar to a critical section. Monitor creates a lock on an object that
does not allow any other threads to obtain access to the object until released
by the locking thread. These locks are on sections of memory, hence the common
name critical section. We will first see how to control access to a block of
code, just like with the SyncLock keyword.
The Enter function of the Monitor class works just like the
SyncLock keyword and the Exit function like the End Synclock keywords.
Internally SyncLock uses the Monitor class to implement its functionality and
generates the inner Try Finally block of the code sample for you. Let us look
at the code now:
Listing 9
Public Sub Foo()
Dim sText As String
Dim objLock As Object = New Object()
Try
Monitor.Enter(objLock)
Try
sText = "Hello"
Finally
Monitor.Exit(objLock)
End Try
Catch e As Exception
MessageBox.Show(e.Message)
End Try
End Sub
This provides the exact same functionality that the above
SyncLock example did. You will also notice that the Exit is contained in the
finally clause of a Try Catch Finally block. This is to ensure that Exit gets
called so the thread will not get locked infinitely. Monitor.Enter is also
called outside of the Try Catch Finally block. This is so Monitor.Exit will
not get called if the Enter method does not, as it will throw another
exception. So why should we use Monitor, if the SyncLock keyword provides the
same functionality without the extra work of Monitor? We will examine the
reasons why Monitor should be used as we look at the other methods of Monitor.
We said earlier that the SyncLock block would wait
indefinitely on the executing thread to release the lock. The Monitor class
provides a much better method to handle this, the TryEnter method. This is the
first reason why you would use Monitor over SyncLock. This method will allow the
calling thread to wait a specific amount of time to acquire a lock before
returning false and stopping its execution. This allows graceful handling of
long running threads or deadlocks. If a deadlock has occurred, you certainly
do not want to add more threads that are trying to get to the deadlocked
resource.
The default method of no parameters will try to acquire a
lock and if unsuccessful it will immediately return false. There are also two
additional overloads that will wait for the specific number of milliseconds, or
the specified TimeSpan. This offers much more flexibility than SyncLock.
Listing 10
Dim objLock As Object = New Object() ‘Object in yourClass
Public Sub Foo()
Dim sText As String
Dim bEnteredOk As Boolean
bEnteredOk = Monitor.TryEnter(objLock, 5000)
If bEnteredOk = True Then
sText = "Hello"
Monitor.Exit(objLock)
End If
End Sub
This example will try to acquire a lock for five seconds.
If successful, the string is set to “Hello."
The rest of Monitor’s methods must be examined together.
The SyncLock keyword and the Monitor.Enter rely on putting waiting threads to
sleep to stop their execution. This is not the best practice to follow as
there is no way to get them to stop waiting unless aborted. The Monitor.Wait
and Monitor.Pulse allow threads to wait on other conditions before starting.
The methods will place the thread in a wait state allowing other threads to
specify when they need the waiting thread to run. An example of this is a
queue. You could have a thread that waits in an idle state until other threads
place objects in the queue for it to work on.
To use the methods, you first tell a thread to wait on an
object with a Monitor.Wait call, like below.
Listing 11
Dim objLock As Object = New Object()
Dim bPulsed As Boolean
Monitor.Enter(objLock)
bPulsed = Monitor.Wait(objLock)
If bPulsed Then
'thread was pulsed
End If
Monitor.Exit(objLock)
The thread is automatically unlocked with the Wait call.
You must be sure to call Monitor.Exit when the thread is pulsed and done with
its work or you will have a block that could result in a deadlock. The first
thread will wait until the pulsing thread has released its lock. This will
make the thread wait until a Monitor.Exit is called, like the following.
Listing 12
Monitor.Enter(objLock)
Monitor.Pulse(objLock)
Monitor.Exit(objLock)
If the Exit call is left off, a block occurs because the
waiting thread cannot obtain its lock on the object that the pulsing thread
has. You must also use the same object to lock on and pulse from the second
thread that the waiting thread used to wait on, objLock. Also, both Wait and
Pulse must be called from a locked block of code, hence the Enter and Exit
calls in the above code. You should exit immediately after calling Pulse to
allow the first thread to perform its work, since the pulsing code has the
current lock on objLock.
The Monitor class also comes equipped with a PulseAll
method. Unlike Pulse, which will only start the next waiting thread, PulseAll
removes the wait state from all waiting threads and allows them to continue
processing. As with the Pulse method, PulseAll must be called from a locked
block of code and on the same object that the original threads are waiting on.
The Monitor class will provide for most of your threading
synchronization needs. It should always be used unless a more specific task
calls for the next few classes we will examine. Here is a review of some good
practices to follow when using Monitor:
1. Exit MUST be called the same number of times Enter
is called or a block will occur.
2. Make sure that the object used to call Enter is the
same object that is used to call Exit or the lock will not be released.
3. Do not call Exit before calling Enter and do not
call Exit more times than calling Enter or an exception will occur.
4. Place the Exit method call in a Finally block. All
code that you wish to lock should be in the Try section of the corresponding
Finally block. The Enter call should be in its own Try block. This eliminates
calling Exit if the Enter fails.
5. Do not call Enter on an object that has been set to
Nothing or an exception will occur.
6. Do not change the object that you use as the locking
object, which brings in number 7.
7. Use a separate locking object and not the changing
object. If you use an object that has changed, an exception will be generated.
MethodImplAttribute
Code attributes in the Dot Net Framework can sometimes make
programming easier. The MethodImplAttribute is one example of the hundreds of
different attributes that you can use. It is in the
System.Runtime.CompilerServices namespace. This attribute is particularly
interesting to synchronization because it can synchronize an entire method with
one simple command.
If you place the attribute before a function and supply the
MethodImplOptions.Synchronized enumeration in the constructor, the entire
method will be synchronized when called. The compiler will create output that
wraps the whole function, MySyncMethod, in a Monitor.Enter and Monitor.Exit
block. When a thread starts to enter the method it will acquire a lock. Upon
exiting the method, it will release the lock. Here is an example of using the
attribute.
Listing 13
<MethodImplAttribute(MethodImplOptions.Synchronized)>
Private Sub MySyncMethod()
End Sub
This attribute should only be used when an entire function
needs to be synchronized, so it is rarely used. If you can exit the
synchronized block of code before the end of the method or wait to enter it to
the middle of the method, Monitor should be used, as the attribute would waste
processing cycles by locking the whole method and not just what needs to be
synchronized.
WaitHandle, AutoResetEvent and ManualResetEvent Classes
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 us 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.
Listing 14
Dim WaitEvent As AutoResetEvent
WaitEvent = New AutoResetEvent(False)
Public Sub DoWork()
Thread.Sleep(5000)
WaitEvent.Set()
End Sub
Public Sub Thread2()
WaitEvent.WaitOne()
End Sub
In the above code, we make a new instance of an
AutoResetEvent. Our main thread 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, thus 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 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 could not 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 will not
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 do not signal all
instances of the AutoResetEvent correctly, as shown below, your waiting thread
will never resume.
Let us take a look at a code example showing how to use
WaitAll. First, the form’s code:
Listing 15
Dim WaitAllEvents(1) As AutoResetEvent
Private Sub Button1_Click(ByVal sender AsSystem.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 mainthread To
‘wait Until all instances of AutoResetEvent havebecome
‘signaled With a Call To Set()
WaitHandle.WaitAll(WaitAllEvents)
Console.WriteLine("All threads done exitingmain 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 myEvent
End Sub
Private Sub Thread2Work()
Thread.Sleep(3000)
Console.WriteLine("Thread2 done")
WaitAllEvents(1).Set()‘I’m done so signal my Event
End Sub
Here is some code in a Module.
Listing 16
<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 that 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
would be a dictionary search engine. The program would 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 us look at a code example.
Listing 17
Dim WaitAnyEvents(1) As AutoResetEvent
Private Sub Start_Click(ByVal sender AsSystem.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 exitingmain 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 does not abort the
first thread, Thread1, it eventually finishes outputting its string “Thread1
done.” If the other threads are no longer needed then they should be aborted
manually from your main thread with a call to Abort.
Now let us 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. The 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 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 us look at some code now.
Listing 18
Private ManualWaitEvent As ManualResetEvent
Dim Thread1 As Thread
Dim sData As String
Private Sub Form1_Load(ByVal sender AsSystem.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 AsSystem.Object, ByVal
e As System.EventArgs) Handles btnSet.Click
sData = "Work Done: "
ManualWaitEvent.Set()
End Sub
Private Sub btnReset_Click(ByVal sender AsSystem.Object,
ByVal e As System.EventArgs) HandlesbtnReset.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 allows you to 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. If
termination of the main running program disposes of a needed resource, the
thread will never finish or result in an error.
Mutex Class
The next class in our list, Mutex, can be thought of as a
more powerful version of Monitor. Like AutoResetEvent and ManualResetEvent, it
is derived from WaitHandle. An advantage of Mutex over Monitor is that you can
use the methods from WaitHandle, such as WaitOne. A disadvantage is that is
much slower, at about half as fast as Monitor. Mutex is very useful when you
must control access to a resource that could be accessed through multiple
processes, like a data file used by several applications you have created. To
write to the file, the writing thread must have total access to the file
throughout the operating system.
When you create a Mutex, you can assign it a name. If the
name exists anywhere in the operating system then that Mutex object instance
will be returned. This is the reason why Mutex is slower. The system must be
checked to see if the Mutex already exists. If it does not exist, a new one is
created. When the last thread in the operating system that references the
named Mutex terminates, the Mutex is destroyed. The following code example
shows how to use a Mutex to control access to a file.
Our first program:
Listing 19
Dim mutexFile As Mutex
Private Sub btnSetMutex_Click(ByVal sender AsSystem.Object, _
ByVal e As System.EventArgs) _
Handles btnSetMutex.Click
mutexFile = New Mutex(False, "MutexName")
mutexFile.WaitOne()
End Sub
Private Sub btnRelease_Click(ByVal sender AsSystem.Object, _
ByVal e As System.EventArgs) _
Handles btnRelease.Click
mutexFile.ReleaseMutex()
End Sub
Our Second Program:
Private Sub btnAquireMutex_Click(ByVal sender As
System.Object, ByVal e As System.EventArgs)Handles
btnAquireMutex.Click
Dim mutexFile As Mutex
mutexFile = New Mutex(False, "MutexName")
mutexFile.WaitOne() ‘Wait Until the File Is Open
Console.WriteLine("Mutex was released fromanother
process")
mutexFile.ReleaseMutex()
End Sub
Let us examine the first program. A Mutex called mutexFile
is created. Internally to the operating system, we name the mutex “Mutex Name.”
This is the name that will be used to resolve any other calls to the same mutex
from any other application that we create. On a form we have two buttons. For
demonstration purposes, one button will acquire a lock on the resource, in this
case the file, using the Mutex and the other button will release the lock.
This simulates a long running process on the file. As with the other
synchronization classes, you should make sure to call RelaseMutex sometime
after a lock is acquired or a block on the resource will occur.
The second program is very straightforward. We create a
Mutex object called fileMutex making sure we have named it the same as in the
first program, “Mutex Name.” If this is not done then the Mutex classes will
refer to different mutexes in the operating system. Then WaitOne is called
without a timeout value. This will make the thread wait until the Mutex has
been released. When the release button is clicked in the first program, the
second can continue running since it can now acquire access to the resource.
Mutex was released from another process and is printed in the output window.
You can also close the first program and the lock will be released. When a
thread exits that has a Mutex lock on a resource, ReleaseMutex is automatically
called for you.
In summary, remember that Monitor should be used most of the
time; it is faster than a Mutex. Mutex should only be used when you need to
synchronize across multiple processes to gain access to a common resource among
several programs that you have written. Even though Mutex allows for the wait
methods where Monitor does not, the other WaitHandle classes should be
considered before Mutex if you need the wait methods first.
ReaderWriterLock Object
Many times, you read data much more often than you write
it. Traditional synchronization can be overkill in these situations, as it
would lock resources when threads are reading or writing to the resource. A
more efficient way has been added to the framework to handle this. The
ReaderWriterLock is a synchronization class that allows multiple threads to
read a variable, but only one thread to write to it at a time.
When acquiring a lock, the write thread must also wait until
all reader threads have unlocked the object before obtaining an exclusive write
lock. All readers will then be blocked until the writer thread releases its
lock. The power of the class comes from the fact that it will allow multiple
reader locks to access the resource at the same time. We will look first at
how to acquire reader locks on an object.
Listing 20
Dim lData As Long = 1
Dim objLock As ReaderWriterLock
Private Sub btnRun_Click(ByVal sender AsSystem.Object, ByVal
e As System.EventArgs) Handles btnRun.Click
Dim Thread1 As Thread
Dim Thread2 As Thread
objLock = New ReaderWriterLock()
Thread1 = New Thread(AddressOf Thread1Work)
Thread2 = New Thread(AddressOf Thread2Work)
Thread1.Start()
Thread2.Start()
End Sub
Private Sub Thread1Work()
Dim i As Integer
For i = 1 To 10
objLock.AcquireReaderLock(1000)
Console.WriteLine(lData & " Thread1")
Thread.Sleep(10)
objLock.ReleaseReaderLock()
Next
End Sub
Private Sub Thread2Work()
Dim i As Integer
For i = 1 To 10
objLock.AcquireReaderLock(1000)
Console.WriteLine(lData & " Thread2")
objLock.ReleaseReaderLock()
Next
End Sub
We create an instance of a ReaderWriterLock object called
objLock. Then two threads are spawned, both of which do a quick loop that
writes the value of lData to the console window ten times. The first thread
also has a ten-millisecond sleep call. This allows us to see that the second
thread continues to get a reader lock on objLock even though the first already
has one. Also, note that we have passed a millisecond time limit to the
methods. You must pass a timeout value to AcquireReaderLock. If you wish to
wait infinitely, use the constant Timeout.Infinite.
The output should be something similar to the following:
1 Thread 1
1 Thread 2
1 Thread 2
1 Thread 2
1 Thread 2
1 Thread 2
1 Thread 2
1 Thread 2
1 Thread 2
1 Thread 2
1 Thread 2
1 Thread 1
1 Thread 1
1 Thread 1
1 Thread 1
1 Thread 1
1 Thread 1
1 Thread 1
1 Thread 1
1 Thread 1
This shows that the second thread ran while the first had a
ReaderLock on the lData integer.
If needed, there is also a method, IsReaderLockHeld, which
will return true if the current thread already has a reader lock. This helps
keep track of multiple locks by one thread. For each call to AcquireReaderLock
a subsequent call to ReleaseReaderLock is required. If you do not call
ReleaseReaderLock the same number of times, the reader lock is never fully
released, never allowing a write to the resource. IsReaderLockHeld can be
checked to see if a reader lock is already active on the thread and if so, not
acquire another one.
Now let us examine how to update the variable. A writer
lock can be obtained by calling AcquireWriterLock. Once all reader locks have
been released, the method will obtain an exclusive lock on the variable. When
updating the variable, all reader threads will be locked out until ReleaseWriterLock
is called. Let us examine the code for this.
Listing 21
Dim lData As Long = 1
Dim objLock As ReaderWriterLock
Private Sub btnRun_Click(ByVal sender AsSystem.Object, ByVal
e As System.EventArgs) Handles btnRun.Click
Dim Thread1 As Thread
Dim Thread2 As Thread
Dim Thread3 As Thread
objLock = New ReaderWriterLock()
Thread1 = New Thread(AddressOf Thread1Work)
Thread2 = New Thread(AddressOf Thread2Work)
Thread3 = New Thread(AddressOf Thread3Work)
Thread1.Start()
Thread2.Start()
Thread3.Start()
End Sub
Private Sub Thread1Work()
Dim i As Integer
For i = 1 To 10
objLock.AcquireReaderLock(1000)
Console.WriteLine(lData & " Thread1")
Thread.Sleep(100)
objLock.ReleaseReaderLock()
Next
End Sub
Private Sub Thread2Work()
Dim i As Integer
For i = 1 To 10
objLock.AcquireReaderLock(1000)
Console.WriteLine(lData & " Thread2")
Thread.Sleep(100)
objLock.ReleaseReaderLock()
Next
End Sub
Private Sub Thread3Work()
objLock.AcquireWriterLock(Timeout.Infinite)
lData = 2
Console.WriteLine("Thread 3 updatedlData")
objLock.ReleaseWriterLock()
End Sub
You will notice that we have added a new thread, Thread3,
and a function for it to run. This new function acquires a writer lock on the
object and then updates lData to 2. The first two threads, Thread1 and
Thread2, are put to sleep for one hundred milliseconds to allow thread three to
start. When examining the output from this code, you will see that thread
three waits until threads one and two release their locks. This thread three
updates the variable. Thread one and two must then wait on it. As with the
reader lock, there is also a method called IsWriterLockHeld that will return
true if the current thread has a writer lock. You should get an output similar
to the one below:
1 Thread 1
1 Thread 2
Thread 3 updated lData
2 Thread 2
2 Thread 1
2 Thread 2
2 Thread 1
2 Thread 2
2 Thread 1
2 Thread 2
2 Thread 1
2 Thread 2
2 Thread 1
2 Thread 2
2 Thread 1
2 Thread 2
2 Thread 1
2 Thread 2
2 Thread 1
2 Thread 2
2 Thread 1
Another useful method of the ReaderWriterLock class is the
UpgradeToWriterLock method. This method allows a reader lock to become a
writer lock to update the data. Sometimes it is useful to check the value of a
data item to see if it should be updated. Acquiring a writer lock to check the
variable is a waste of time and processing power. By getting a reader lock
first, the other reader threads are allowed to continue accessing the variable
until you determine an update is needed. Once the update is needed,
UpgradeToWriterLock is called and locks the resource for update as soon as it
can acquire the lock. Just like AcquireWriterLock, UpgradeToWriterLock must
wait until all readers accessing the resource are done. Now we will look at
the code.
Listing 22
Dim lData As Long = 1
Dim objLock As ReaderWriterLock
Private Sub btnRun_Click(ByVal sender AsSystem.Object, ByVal
e As System.EventArgs) Handles btnRun.Click
Dim Thread1 As Thread
Dim Thread2 As Thread
objLock = New ReaderWriterLock()
Thread1 = New Thread(AddressOf Thread1Work)
Thread2 = New Thread(AddressOf Thread2Work)
Thread1.Start()
Thread2.Start()
End Sub
Private Sub Thread1Work()
Dim i As Integer
For i = 1 To 10
objLock.AcquireReaderLock(1000)
If lData = i Then
objLock.UpgradeToWriterLock(Timeout.Infinite)
lData = i + 1
Console.WriteLine("lData is now "& lData)
End If
Thread.Sleep(20)
objLock.ReleaseReaderLock()
Next
End Sub
Private Sub Thread2Work()
Dim i As Integer
For i = 1 To 10
objLock.AcquireReaderLock(1000)
Console.WriteLine(lData & " Thread2")
Thread.Sleep(20)
objLock.ReleaseReaderLock()
Next
End Sub
In this example, we have changed thread one to examine the
value of lData after acquiring a reader lock. If the value of lData is equal
to the looping variable of i (which it always is in our example) then it tries
to obtain a writer lock by calling UpgradeToWriterLock. Nothing special is required
to release the writer lock once finished with it. The normal ReleaseReaderLock
will release the upgraded writer lock or calling DowngradeFromWriterLock can be
used to release the lock, which will be discussed next. The output should be
something similar to the following:
lData is now 2
2 Thread 2
lData is now 3
3 Thread 2
lData is now 4
4 Thread 2
lData is now 5
5 Thread 2
lData is now 6
6 Thread 2
lData is now 7
7 Thread 2
lData is now 8
8 Thread 2
lData is now 9
9 Thread 2
lData is now 10
10 Thread 2
lData is now 11
11 Thread 2
Opposite of UpgradeToWriterLock we can also use
DowngradeFromWriterLock. Like its name suggests, the method will make a writer
lock turn to a reader lock. To use the function, you must pass it a
LockCookie. This cookie can be generated from UpgradeToWriterLock. Because of
the LockCookie requirement, you may only use DowngradeFromWriterLock on the
same thread that UpgradeToWriterLock is called.
One advantage of DowngradeFromWriterLock is that the call
returns immediately and will not block the thread at all. This happens because
it can only be called from a thread that has a writer lock on an object. This
means that no other thread can have a lock; the method knows that it is the
only thread active on the object. If read access is still required to the
resource, this method will eliminate the need to reacquire a read lock on the
thread. If read access is not required anymore, simply use ReleaseReaderLock,
as shown above. Examine the code below.
Listing 23
Dim lData As Long = 1
Dim objLock As ReaderWriterLock
Private Sub btnRun_Click(ByVal sender AsSystem.Object, ByVal
e As System.EventArgs) Handles btnRun.Click
Dim Thread1 As Thread
Dim Thread2 As Thread
objLock = New ReaderWriterLock()
Thread1 = New Thread(AddressOf Thread1Work)
Thread2 = New Thread(AddressOf Thread2Work)
Thread1.Start()
Thread2.Start()
End Sub
Private Sub Thread1Work()
Dim i As Integer
Dim objCookie As LockCookie
For i = 1 To 10
objLock.AcquireReaderLock(1000)
If lData = i Then
objCookie =
objLock.UpgradeToWriterLock(Timeout.Infinite)
lData = i + 1
Console.WriteLine("lData is now "& lData)
objLock.DowngradeFromWriterLock(objCookie)
Console.WriteLine("Downgraded lock")
End If
Thread.Sleep(20)
objLock.ReleaseReaderLock()
Next
End Sub
Private Sub Thread2Work()
Dim i As Integer
For i = 1 To 10
objLock.AcquireReaderLock(1000)
Console.WriteLine(lData & " Thread2")
Thread.Sleep(20)
objLock.ReleaseReaderLock()
Next
End Sub
The only differences in this code from the
UpgradeToWriterLock are the lines:
objCookie = objLock.UpgradeToWriterLock(Timeout.Infinite)
objLock.DowngradeFromWriterLock(oCookie)
Console.WriteLine("Downgraded lock")
Instead of just waiting until the ReleaseReaderLock is
called, we explicitly change the writer lock to a reader lock. The only real
difference between downgrading and releasing the lock are with any other
waiting writer locks. If you downgrade and still have waiting writer locks,
they must continue to wait until the downgraded lock is released. You should
see output similar to the following:
1 Thread 2
lData is now 2
Downgraded lock
2 Thread 2
lData is now 3
Downgraded lock
3 Thread 2
lData is now 4
Downgraded lock
4 Thread 2
lData is now 5
Downgraded lock
5 Thread 2
lData is now 6
Downgraded lock
6 Thread 2
6 Thread 2
lData is now 7
Downgraded lock
7 Thread 2
lData is now 8
Downgraded lock
8 Thread 2
lData is now 9
Downgraded lock
9 Thread 2
lData is now 10
Downgraded lock
lData is now 11
Downgraded lock
Two other methods of note on the ReaderWriterLock class are
ReleaseLock and RestoreLock. ReleaseLock immediately drops all locks that the
current thread holds. It returns a LockCookie just like UpgradeToWriterLock
that can be used in RestoreLock. When used, the LockCookie returns the thread
back to the exact lock state that it held before. To handle the fact that
other threads could have acquired locks on the object, the method will block
until it can resolve all of its previous locks. The code is as follows:
Listing 24
Dim oLock As ReaderWriterLock
Private Sub btnRun_Click(ByVal sender AsSystem.Object, ByVal
e As System.EventArgs) Handles btnRun.Click
Dim Thread1 As Thread
Dim objCookie As LockCookie
objLock = New ReaderWriterLock()
Thread1 = New Thread(AddressOf Thread1Work)
objLock.AcquireWriterLock(Timeout.Infinite)
Thread1.Start()
Thread.Sleep(1000)
objCookie = objLock.ReleaseLock
Thread1 = New Thread(AddressOf Thread1Work)
Thread1.Start()
Thread.Sleep(1000)
objLock.RestoreLock(oCookie)
Thread.Sleep(1000)
Thread1 = New Thread(AddressOf Thread1Work)
Thread1.Start()
End Sub
Private Sub Thread1Work()
Try
objLock.AcquireReaderLock(10)
Console.WriteLine("Got a reader lock")
objLock.ReleaseReaderLock()
Catch
Console.WriteLine("Reader lock notheld")
End Try
End Sub
Examining the code, we first see that a writer lock is
acquired. Thread1 is then started to show that it cannot acquire a reader lock
on the object. The main thread then releases the writer lock by calling
ReleaseLock and saving its state to objCookie. Thread1 is then restarted acquiring
the reader lock. RestoreLock is called with the LockCookie then passed to it.
When thread one is restarted at that point it cannot acquire its reader lock.
The call to RestoreLock has replaced the writer lock on the object. The output
looks like the following:
Reader lock not held
Got a reader lock
Reader lock not held
Another interesting pair of functions in the ReaderWriterLock
class is the WriterSeqNum and AnyWritersSince. WriterSeqNum returns the
sequence number of the current lock in the internal queue of the
ReaderWriterLock class. This queue keeps the order of the threads that have
requested reader or writer locks on an object. AnyWritersSince can tell if any
writer locks have been released since the call to WriterSeqNum. This is a good
method to check if a piece of data has been updated on another thread.
AnyWritersSince could be used in a large, time-consuming report situation. If
no writers have updated the report data then there is no need to recalculate
the report. The following code will show the methods in action.
Listing 25
Dim objLock As ReaderWriterLock
Private Sub btnRun_Click(ByVal sender As System.Object,ByVal
e As System.EventArgs) Handles btnRun.Click
Dim objCookie As LockCookie
Dim SeqNum As Integer
Dim Thread1 As Thread
objLock = New ReaderWriterLock()
Thread1 = New Thread(AddressOf Thread1Work)
objLock.AcquireWriterLock(Timeout.Infinite)
SeqNum = objLock.WriterSeqNum
If objLock.AnyWritersSince(SeqNum) = False Then
Console.WriteLine("We see that no writershave
released yet")
End If
objLock.ReleaseWriterLock()
Thread1.Start()
Thread1.Join()
If objLock.AnyWritersSince(SeqNum) = True Then
Console.WriteLine("We see that a writer hasreleased
Now")
End If
End Sub
Public Sub Thread1Work()
objLock.AcquireWriterLock(Timeout.Infinite)
objLock.ReleaseWriterLock()
End Sub
First a writer lock is acquired on objLock. The sequence
number is saved in SeqNum. Then a test to AnyWritersSince is made. Since no
other threads have acquired any writer locks and released them, the method
returns false. Next a thread, Thread1, is started and waited on. This thread
simply acquires a writer lock and releases it. The main thread then checks
AnyWritersSince again using the saved off sequence number. Since another
thread has released a writer lock the method returns true this time. The
following output is returned.
We see that no writers have released yet
We see that a writer has released now
COM+ Synchronization
The dot net framework provides many enterprise services that
can be used to build enterprise applications, one of which is the COM+ method
of synchronization. COM+ offers developers many helpful techniques, such as
transaction handling between objects, loosely coupled events, object pooling
and synchronization, which we will discuss here. This synchronization method
allows the usage of a concept called a context to provide ways to lock code for
synchronization. This method can be implemented on any class that is derived
from ContextBoundObject or from any class that derives from
ContextBoundObject.
When deriving a class from ContextBoundObject, the attribute
<Synchronization()> can be used. This tells the runtime to provide
synchronization for the entire class by making each class instance only
accessible by one thread at a time. This case study will give a brief overview
of this topic, as it is out of the scope of the article. Entire books have
been written on the subject of COM+. For further reading on COM+ get a copy of
Professional Visual Basic Interoperability – COM and VB6 to .NET, ISBN
1-861005-65-2.
When you use the attribute, COM+ will create a proxy for you
that will run all instances of your object in its context. COM+ will marshal
all calls across this proxy where a performance penalty occurs. The service
guarantees that only one thread is available to run each object at a time.
Earlier, the timed methods of the WaitHandle classes were
discussed. Recall that the second parameter of the method was a boolean method
that determined whether to release the synchronized context along with the
object lock. If your classes use COM+ synchronization, True should be passed
for this parameter or deadlocks are risked. True tells COM+ to exit its
synchronized context before the runtime allows the thread to wait. This allows
other threads to then get access to the context avoiding deadlocks. If you do
not exit the context, the .Net runtime will allow other threads access to the
locked object since an exit method has been called. When the next thread
acquires a lock on the locking object it will then try to enter the context,
which is still locked, resulting in a deadlock.
While COM+ synchronization provides another easy way to
provide synchronization, be careful when using it. Many calls to a COM+
synchronized object will degrade your application greatly because of all the
marshaling across the proxy. Be sure to test responsiveness when using it.
Apartments and Window’s Form Synchronization
Now that we have examined all the methods that Visual Basic
offers for synchronization, we will take a look at Window’s Form projects and
what apartment threading is. The most common types of threading on the Windows
platform are single threaded apartments (STA) or multithreaded apartments
(MTA). Window’s forms must be hosted in an STA apartment because some Window’s
Form controls are based on standard Windows COM controls that require an STA
environment. Background threads can still be utilized to update forms, but
synchronization must be done differently. As we examine the two apartment
styles, we will look at how to do correct synchronization with Window’s Forms.
By default, all Windows’ Form projects in Visual Basic are
STA. Visual Basic applies the <STAThread()> attribute to the main entry
point in the application for you behind the scenes. While you could override
this attribute and change it to an MTA apartment, you should not or problems
will occur with the COM controls.
So what is an STA apartment? The apartment concept comes
from the early COM days. Basically, STA means that only one thread can access
an object, the thread that created it. Any future access to the object must
also be done on the original thread. This is the key reason why you should
never update a control on a Window’s Form from another thread. Most COM
objects require STA.
MTA, sometimes called free threading, is much harder to
program than STA. This is another reason why we encounter STA COM components
most of the time. MTA means that more than one thread can access an object at
any given point in time safely. When programming for MTA, you must be sure to
include good synchronization and design as discussed in the case study. Any
number of threads could be accessing objects in your library at any time.
The type of threading model that the current thread is using
can be determined simply with the following code.
Listing 26
Dim sThreadType As String
sThreadType =Thread.CurrentThread.ApartmentState.ToString()
MessageBox.Show(sThreadType)
Dim Apt as ApartmentState
Apt = Thread.CurrentThread.ApartmentState()
MessageBox.Show(apt.ToString())
Window’s Form classes provide built in methods to update GUI
elements from other threads. These methods should be used exclusively. The
methods are called Invoke, BeginInvoke, EndInvoke and CreateGraphics. All of
the methods can be called from any thread. When called, the methods provide a
way to work with the control from the main Window’s Form thread. Let us see
how we can use the methods.
The Invoke method takes a delegate for a parameter. A
delegate is basically a variable that points to a method. The variable in this
case tells the Invoke method what function to run. This delegate is run under
the control’s owner thread and not the calling thread, preserving the STA
style. Let us take a look at a simple example that adds entries to a textbox
control using a separate thread. A button and a multi-line textbox are added
to a Window’s Form.
Listing 27
Private Sub btnStart_Click(ByVal sender AsSystem.Object, ByVal
e As System.EventArgs) Handles btnStart.Click
Dim Thread1 As Thread
Thread1 = New Thread(AddressOf Thread1Work)
Thread1.Start()
End Sub
Private Delegate Sub DelAddItem()
Private Sub Thread1Work()
Dim del As DelAddItem
del = New DelAddItem(AddressOf DelegateWork)
txtList.Invoke(del)
Console.WriteLine("Thread 1 Done")
End Sub
Private Sub DelegateWork()
Dim i As Integer
For i = 0 To 100
txtList.Text = txtList.Text + "A New Line:" &
i.ToString() + vbCrLf
Next 'i
Console.WriteLine("Delegate Done")
End Sub
To call Invoke, a delegate sub is created. This sub simply
adds a new line to the textbox with the words “A New Line.” When our new
thread is started, a new instance of the delegate is created. The new delegate
is then passed to txtList.Invoke updating the text.
The Invoke method runs any code in the delegate
synchronously on the thread. The output from the run will show this:
Delegate Done
Thread 1 Done
Thread 1 will not continue running until the delegateis finished.
Sometimes asynchronous calls are preferred. The BeginInvoke
and EndInvoke allow for updating the GUI using built in asynchronous technology
in the framework. The two methods take the same delegate that Invoke did.
They only call the code asynchronously. EndInvoke will return the resulting
value from an asynchronous BeginInvoke call. If the BeginInvoke is still
running, EndInvoke will block until the BeginInvoke call finishes. It will not
terminate the BeginInvoke call. An example is below.
Listing 28
Private Sub btnStart_Click(ByVal sender AsSystem.Object, _
ByVal e As System.EventArgs) Handles btnStart.Click
Dim Thread1 As Thread
Thread1 = New Thread(AddressOf Thread1Work)
Thread1.Start()
End Sub
Private Delegate Sub DelAddItem()
Private Sub Thread1Work()
Dim del As DelAddItem
Dim Result As IAsyncResult
del = New DelAddItem(AddressOf DelegateWork)
Result = txtList.BeginInvoke(del)
Console.WriteLine("Thread 1 Done")
Console.WriteLine(Result.IsCompleted.ToString())
txtList.EndInvoke(Result)
Console.WriteLine(Result.IsCompleted.ToString())
End Sub
Private Sub DelegateWork()
Dim i As Integer
For i = 0 To 100
txtList.Text = txtList.Text + "A New Line:" &
i.ToString() + vbCrLf
Next 'i
Console.WriteLine("Delegate Done")
End Sub
Output:
Thread 1 Done
False
Delegate Done
True
As we see from the output, Thread 1 completed before the
delegate finished. Then the first call to Result.IsCompleted returns false,
signifying that the delegate is still running. Thread 1 is then put to sleep
with the EndInvoke call, allowing the delegate time to finish. The next call
to Result.IsCompleted returns true.
The code also shows two methods of getting the status of an
asynchronous call. The first method was the line Result = txtList.BeginInvoke(del). The Result variable will contain the current results of the asynchronous call. The
other method is with the EndInvoke call, which as we said earlier, would block
until the asynchronous call is finished. The last output of true shows that
this behavior happened.
When using graphics drawing methods with Window’s Forms you
must be sure to do all work on the main thread as well. The CreateGraphics
method makes sure of this for you. It can be called from other threads safely
like the invoke methods. The Graphics object returned will run all calls in
the correct thread for you. The Graphics object is considered thread safe so
no additional locking objects are necessary.
A Quick Word on the volatile Keyword
In your reading or study of .Net code, the volatile C#
keyword might come up. This keyword does not exist in Visual Basic. Do not
worry though; it does not add any functionality to C# that cannot be done with
the other synchronization objects discussed in this case study.
The volatile keyword tells the compiler that the variable it
references could change at any moment and that no optimizations should be done
to it. It will prohibit the compiler from storing the variable in a register
and force it to read it new from memory each time.
Variables marked as volatile are not necessarily thread
safe. They only insure that each read of the variable is the latest
information. To see what a declaration looks like, look at the following code
snip-it, which declares an Integer variable as volatile.
The use of Monitor is a much better and safer way to handle
synchronization. It guarantees that the variable is up to date as only one
thread is accessing the variable at a time. It is safe to replace volatile
variable access with Monitor blocks of code or any other synchronization method
discussed in the case study that fit your needs. Good synchronization practice
will eliminate the need for volatile.
Summary
Multithreaded applications are a must today. The Dot Net
Framework makes creating these applications much easier than traditional
programming methods. Be sure to take advantage of multithreading and of all
available methods of synchronization.
When designing for multithreaded applications remember the
age-old proverb: An ounce of prevention is worth a pound of cure. It is much
easier to prevent deadlocks and other multithreaded bugs by taking a few extra
minutes to prevent them. You will most likely spend a lot of time trying to
find the cause of these bugs when reported from the field, as they do not
usually show up stepping through code, but only when running at full speed.