A Thread to Visual Basic
Contents:
Introduction.
A quick Review of Multithreading.
A multithreading Simulator.
Avoiding Multithreading Problems.
Visual Basic Service pack #2.
Why Multithread?
The Threading Contract.
The CreateThread API.
The Create Thread API Revisited.
Conclusion.
VB6 update.
SpyWorks 6.2 update.
For Further Reference.
You can download the sample code from
ftp.desaware.com/SampleCode/Articles/Thread.zip
Just because you can do something, doesn't always mean that you should.
With the appearance of the AddressOf operator, an entire industry has
developed among authors illustrating how to do previously impossible tasks
using Visual Basic. Another industry is rapidly developing among consultants
helping users who have gotten into trouble attempting these tasks.
The problem is not in Visual Basic or in the technology. The problem lies in
the fact that many authors are applying the same rule to AddressOf
techniques that many software companies apply to software in general -- if
you can do something, you should. The idea that the newest and latest
technology must, by definition, be the best solution to a problem is
prevalent in our industry. This idea is wrong. Deployment of technology
should be driven primarily by the problem that you are trying to solve, not
by the technology that someone is trying to sell you.
Worse yet, just as companies often neglect to mention the limitations and
disadvantages of their tools, authors sometimes fail to stress the
consequences of some of the techniques that they describe. And magazines and
books sometimes neglect their responsibility to make sure that the
programming practices that they describe are sound.
As a programmer, it is important to choose the right tool for the job. It is
your responsibility to develop code that not only works now under one
particular platform, but that works under all target platforms and system
configurations. Your code must be well documented and supportable by those
programmers who follow you on the project. Your code must follow the rules
dictated by the operating system or standards that you are using. Failure to
do so can lead to problems in the future as systems and software are
upgraded.
Recent articles in the Microsoft Systems Journal and Visual Basic
Programmer's Journal introduced to Visual Basic programmers the possibility
of using the CreateThread API function to directly support multithreading
under Visual Basic. In fact, one reader went so far as to contact me and
complain that my Visual Basic Programmer's Guide to the Win32 API was
fatally flawed because I did not cover this function or demonstrate this
technique. This article is in part a response to this reader, and in part a
response to other articles written on the subject. This article also serves,
in part, as an update to chapter 14 of my book "Developing ActiveX
Components with Visual Basic 5.0: A Guide to the Perplexed" with regards to
new features supported by Visual Basic 5.0 Service Pack 2.
A Quick Review of Multithreading
If you are already well versed in multithreading technology, you may wish to
skip this section and continue from the section titled "The Threading
Contract" or "New for Service Pack 2."
Everyone who uses Windows knows that it is able to do more than one thing at
a time. It can run several programs simultaneously, while at the same time
playing a compact disk, sending a fax, and transferring a file. Every
programmer knows (or should know) that the computer's CPU can only execute
one instruction at a time (we'll ignore the existence of multiprocessing
machines for the time being). How can a single CPU do multiple tasks?
It does this by rapidly switching among the different tasks. The operating
system holds all of the programs that are running in memory. It allows the
CPU to run each program in turn. Every time it switches between programs, it
swaps the internal register values including the instruction pointer and
stack pointer. Each of these "tasks" is called a thread of execution.
In a simple multitasking system, each program has a single thread of
execution. This means that the CPU starts executing instructions at the
beginning of the program, and continues following the instructions in the
sequence defined by the program until the program terminates.
Let's say the program has five instructions: A B C D and E that execute in
sequence (no jumps in this example). When an application has a single
thread, the instructions will always execute in exactly the same order: A,
B, C, D and E. True, the CPU may take time off to execute other instructions
in other programs, but they will not effect this application unless there is
a conflict over shared system resources -- another subject entirely.
An advanced multithreading operating system such as Windows allows an
application to run more than one thread at a time. Let's say that
instruction D in our sample application had the ability to create a new
thread that started at instruction B and ran through the sequence C and E.
The first thread would still be A, B, C, D, E, but when D executed a new
thread would begin that would execute B, C, E (we don't want to execute D
again or we'll get another thread).
Exactly what order will the instructions follow in this application?
It could be:
Thread 1
A
B
C
D
E
Thread 2
B
C
E
Or it could be:
Thread 1
A
B
C
D
E
Thread 2
B
C
E
or perhaps:
Thread 1
A
B
C
D
E
Thread 2
B
C
E
In other words, when you start a new thread of execution in an application,
you can never know the exact order in which instructions in the two threads
will execute relative to each other. The two threads are completely
independent.
Why is this a problem?
A Multithreading Simulator
Consider the MTDemo project:
(You can download the sample code from
ftp.desaware.com/SampleCode/Articles/Thread.zip)
The project contains a single code module that contains two global variables
as follows:
' MTDemo - Multithreading Demo program
' Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Public GenericGlobalCounter As Long
Public TotalIncrements As Long
It contains a single form named frmMTDemo1 which contains the following
code:
' MTDemo - Multithreading Demo program
' Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Dim State As Integer
' State = 0 - Idle
' State = 1 - Loading existing value
' State = 2 - Adding 1 to existing value
' State = 3 - Storing existing value
' State = 4 - Extra delay
Dim Accumulator As Long
Const OtherCodeDelay = 10
Private Sub Command1_Click()
Dim f As New frmMTDemo1
f.Show
End Sub
Private Sub Form_Load()
Timer1.Interval = 750 + Rnd * 500
End Sub
Private Sub Timer1_Timer()
Static otherdelay&
Select Case State
Case 0
lblOperation = "Idle"
State = 1
Case 1
lblOperation = "Loading Acc"
Accumulator = GenericGlobalCounter
State = 2
Case 2
lblOperation = "Incrementing"
Accumulator = Accumulator + 1
State = 3
Case 3
lblOperation = "Storing"
GenericGlobalCounter = Accumulator
TotalIncrements = TotalIncrements + 1
State = 4
Case 4
lblOperation = "Generic Code"
If otherdelay >= OtherCodeDelay Then
State = 0
otherdelay = 0
Else
otherdelay = otherdelay + 1
End If
End Select
UpdateDisplay
End Sub
Public Sub UpdateDisplay()
lblGlobalCounter = Str$(GenericGlobalCounter)
lblAccumulator = Str$(Accumulator)
lblVerification = Str$(TotalIncrements)
End Sub
This program uses a timer and a simple state machine to simulate
multithreading. The State variable describes the five instructions that this
program executes in order. State zero is an idle state. State one loads
local variable with the GenericGlobalCounter global variable. State two
increments the local variable. State three stores the result into the
GenericGlobalCounter variable and increments the TotalIncrements variable
(which counts the number of times that the GenericGlobalCounter variable has
been incremented). State 4 adds an additional delay representing time spent
running other instructions in the program.
The UpdateDisplay function updates three labels on the form that show the
current value of the GenericGlobalCounter variable, the local accumulator,
and the total number of increments.
Each timer tick represents a CPU cycle on the current thread. If you run the
program you'll see that the value of the GenericGlobalCounter variable will
always be exactly equal to the TotalIncrements variable -- which makes
sense, because the TotalIncrements variable shows the number of times the
thread has incremented the GenericGlobalCounter.
But what happens when you click the Command1 button and start a second
instance of the form? This new form simulates a second thread.
Every now and then, the instructions will line up in such a way that both
forms load the same GenericGlobalCounter value, increment it, and store it.
As a result, the value will only increase by one, even though each thread
believed that it had independently incremented the variable. In other
words -- the variable was incremented twice, but the value only increased by
one. If you launch several forms you will quickly see that the number of
increments as represented by the TotalIncrements variable grows much more
rapidly than the GenericGlobalCounter variable.
What if the variable represents an object lock count - which keeps track of
when an object should be freed? What if it represents a signal that
indicates that a resource is in use?
This type of problem can lead to resources becoming permanently unavailable
to the system, to object being locked internally in memory, or freed
prematurely. It can easily lead to application crashes.
This example was designed to make the problem easy to see -- but try
experimenting with the value of the OtherCodeDelay variable. When the
dangerous code is relatively small compared to the entire program, problems
will appear less frequently. While this may sound good, the opposite is
true. Multithreading problems can be extremely intermittent and difficult to
find. This means that multithreading demands careful design up front.
Avoiding Multithreading Problems
There are two relatively easy ways to avoid multithreading problems.
Avoid all use of global variables.
Add synchronization code wherever global variables are used.
The first approach is the one used by Visual Basic. When you turn on
multithreading in a Visual Basic applications, all global variables become
local to a specific thread. This is inherent in the way Visual Basic
implements apartment model threading -- more on this later.
The original release of Visual Basic 5.0 only allowed multithreading in
components that had no user interface elements. This was because they had
not figured out at the time a way to make the forms engine thread safe. For
example: when you create a form in Visual Basic, VB gives it an implied
global variable name (thus if you have a form named Form1, you can directly
access its methods using Form1.method instead of declaring a separate form
variable). This type of global variable can cause the kinds of
multithreading problems you saw earlier. There were undoubtedly other
problems within the forms engine as well -- making a package that complex
safe for multithreading can be quite a challenge.
With service pack 2, Visual Basic's forms engine was made thread safe. One
sign of this is that each thread has its own implied global variable for
each form defined in the project.
New for Service Pack 2
By making the forms engine thread safe, Service pack 2 made it possible for
you to create multithreading client applications using Visual Basic. This is
demonstrated in the MTDemo2 project:
(You can download the sample code from
ftp.desaware.com/SampleCode/Articles/Thread.zip)
The application must be defined as an ActiveX Exe program with startup set
to Sub Main in a code module as follows:
' MTDemo2 - Multithreading demo program
' Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Declare Function FindWindow Lib "user32" Alias "FindWindowA"
(ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Sub Main()
Dim f As frmMTDemo2
' We need this because Main is called on each new thread
Dim hwnd As Long
hwnd = FindWindow(vbNullString, "Multithreading Demo2")
If hwnd = 0 Then
Set f = New frmMTDemo2
f.Show
Set f = Nothing
End If
End Sub
The first time through, the program loads and displays the main form of the
application. The Main routine needs some way of finding out whether this is
the first thread of the application because it is executed at the start of
every thread. You can't use a global variable to find this out because the
Visual Basic apartment model keeps global variables specific to a single
thread. In this example the FindWindow API function is used to check if the
main form of the example has been loaded. There are other ways to find out
if this is the main thread, including use of system synchronization
objects - but this too is a subject for another time and place.
Multithreading is accomplished by creating an object in a new thread. The
object must be defined using a class module. In this case, a simple class
module is defined as follows:
' MTDemo2 - Multithreading demo program
' Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Private Sub Class_Initialize()
Dim f As New frmMTDemo2
f.Show
Set f = Nothing
End Sub
We can set the form variable to nothing after it is created because the act
of
showing the form will keep it loaded.
' MTDemo2 - Multithreading demo program
' Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Private Sub cmdLaunch1_Click()
Dim c As New clsMTDemo2
c.DisplayObjPtr Nothing
End Sub
Private Sub cmdLaunch2_Click()
Dim c As clsMTDemo2
Set c = CreateObject("MTDemo2.clsMTDemo2")
End Sub
Private Sub Form_Load()
lblThread.Caption = Str$(App.ThreadID)
End Sub
The form displays its thread identifier in a label on the form. The form
contains two launch buttons, one that uses the New operator, the other that
uses the CreateObject operator.
If you run the program within the Visual Basic environment, you'll see that
the forms are always created in the same thread. This is because the Visual
Basic environment only supports a single thread. If you compile the program,
you'll see that the CreateObject approach creates both the clsMTDemo2 and
its form in a new thread.
Why Multithread?
Why all the fuss about multithreading if there is so much potential danger
involved? Because, in certain situations, multithreading can dramatically
improve performance. In some cases it can improve the efficiency of certain
synchronization operations such as waiting for an application to terminate.
It allows more flexibility in application architecture. For example, Add a
long operation to the form in the MTDemo2 application with code such as
this:
Private Sub cmdLongOp_Click()
Dim l&
Dim s$
For l = 1 To 1000000
s = Chr$(l And &H7F)
Next l
End Sub
Launch several instances of the form using the cmdLaunch1 button. When you
click on the cmdLongOp button on any of the forms, you will see that it
freezes up operations on all of the other forms. This is because all of the
forms are running on a single thread -- and that thread is busy running the
long loop. If you reproduce this using the cmdLaunch2 button (with a
compiled executable) and click the cmdLongOp button on a form, only that
form will be frozen -- the other forms will continue to be active. They are
running in their own execution thread, and the long loop operation only ties
up its own thread. Of course, you probably shouldn't be placing these kinds
of long operations in your forms in any case.
Here is a brief summary of when multithreading has value:
ActiveX EXE Server -- no shared resources.
When you have an ActiveX EXE server that you expect to share among
applications, multithreading prevents the applications from interfering with
each other. If one application performs a long operation on an object in a
single threaded server, the other applications are frozen out waiting for
the server to become available. Multithreading avoids this problem. However,
there are cases where you may want to use an ActiveX EXE server to arbitrate
access to a shared resource. An example of this is the stock quote server
described in my Developing ActiveX Components book. In this case the single
thread runs the stock quote server which is shared among all of the
applications using the server in turn.
Multithreading Client -- Implemented as an ActiveX EXE Server
A simple form of this approach is demonstrated in the MTDemo2 application.
It is used when an application supports multiple windows that must exit
within a single application but work completely independently. Internet
browsers are a good example of multithreaded clients, where each browser
window runs in its own thread. Note that multithreading should not be used
as a substitute for good event driven design.
Multithreading DLL
A multithreading DLL does not actually create its own threads. It is simply
a DLL that creates objects that run in the same thread that requests the
objects. For example: a multithreaded ActiveX control (which is a DLL)
creates controls that run in the same thread as the form that contains the
control. This can improve efficiency on a multithreaded client such as an
Internet browser.
Multithreaded Servers DLL or EXE
In a client server architecture, multithreading can improve performance if
you have a mix of long and short client requests. Be careful though -- if
all of your client requests are of similar length, multithreading can
actually slow down the server's average response time! Never assume that the
fact that your server is multithreading will necessarily improve
performance.
The Threading Contract
Believe it or not, all of this has been in the way of introduction. Some of
the material reviews information that is covered in far greater depth in my
Developing ActiveX Components book, other material describes new information
for service pack 2.
Now, allow me to ask a question that goes directly to the heart of
multithreading using COM (the component object model on which all Visual
Basic objects, and those in other windows applications that use OLE are
based).
Given:
That multithreading is potentially extremely dangerous and specifically --
that attempting to multithread code that is not designed to support
multithreading is likely to cause fatal errors and system crashes.
Question:
How is it possible that Visual Basic allows you to create objects and use
them under both single and multithreaded environments without any regard to
whether they are designed for single or multithreaded use?
In other words -- How can a multithreaded Visual Basic application use
objects that are not designed to be thread safe? How can other multithreaded
applications use single threaded Visual Basic objects?
In short: how does COM handle threading issues?
If you know about COM, you know that it defines the structure of a contract.
A COM object agrees to follow certain rules so that it can be used
successfully from any application or object that supports COM.
Most people think first of the interface part of the contract -- the methods
and properties that an object exposes.
But you may not be aware of the fact that COM also defines threading as part
of the contract. And like any part of the COM contract -- if you break it,
you are in very deep trouble.
Visual Basic, naturally, hides most of this from you, but in order to
understand what follows, you must learn a little bit about the COM threading
models.
The Single Threading Model:
A single threaded server is the simplest type of server to implement. It is
also the easiest to understand. In the case of an EXE server, the server
runs in a single thread. All objects are created in that thread. All method
calls to each object supported by the server must arrive in that thread.
But what if the client is running in a different thread? In that case, a
proxy object must be created for the server object. This proxy object runs
in the client's thread and reflects the methods and properties of the actual
object. When a method is called on the proxy object, it performs whatever
operations are necessary to switch to the object's thread, then calls the
methods on the actual object using the parameters passed to the proxy.
Naturally, this is a rather time consuming task -- but it does allow the
contract to be followed. This process of switching threads and transferring
data from the proxy object, to the actual object and back is called
marshalling. It is covered in more depth in chapter 6 in my Developing
ActiveX Components book.
In the case of DLL servers, the single threading model demands that all
objects in the server be created and called in the same thread as the first
object created by the server.
The Apartment Threading Model
Note that apartment model threading as defined by COM does NOT require that
each thread have its own set of global variables. That's just how Visual
Basic implemented the apartment model. The apartment model states that each
object may be created in its own thread, however once an object is created,
its methods and properties may only be called by the same thread that
created the object. If another thread wants to access methods of the object,
it must go through a proxy.
This is a relatively easy model to implement. If you eliminate global
variables (as Visual Basic does), the apartment model grants you thread
safety automatically -- since each object is effectively running in its own
thread, and due to the lack of global variables, the different object
threads do not interact with each other.
The Free Threading Model
The free threading model basically says that all bets are off. Any object
can be created in any thread. All methods and properties on any object can
be called at any time from any thread. The object accepts full
responsibility for handling any necessary synchronization.
This is the hardest model to implement successfully, since it demands that
the programmer handle all synchronization. In fact, until recently, OLE
itself did not support this threading model! However, since marshalling is
never required, this is the most efficient threading model.
Which model does your server support?
How does an application, or Windows itself, know which threading model a
server is using? This information is included in the registry. When Visual
Basic creates and object, it checks the registry to determine in which cases
a proxy object and marshalling are required.
It is the client's responsibility to adhere strictly to the threading
requirements of each object that it creates.
The CreateThread API
Now let's take a look at how the CreateThread API can be used with Visual
Basic.
Say you have a class that you want to have running in another thread in
order to perform some background operation. A generic class of this type
might have the following code (from the MTDemo 3 example):
' Class clsBackground
' MTDemo 3 - Multithreading example
' Copyright © 1997 by Desaware Inc. All Rights Reserved
Option Explicit
Event DoneCounting()
Dim l As Long
Public Function DoTheCount(ByVal finalval&) As Boolean
Dim s As String
If l = 0 Then
s$ = "In Thread " & App.threadid
Call MessageBox(0, s$, "", 0)
End If
l = l + 1
If l >= finalval Then
l = 0
DoTheCount = True
Call MessageBox(0, "Done with counting", "", 0)
RaiseEvent DoneCounting
End If
End Function
The class is designed so that the DoTheCount function can be called
repeatedly from a continuous loop in the background thread. We could have
placed the loop in the object itself, but you'll see shortly that there are
sound reasons for designing the object as shown here. The first time the
DoTheCount function is called, a MessageBox appears showing the thread
identifier -- that way we can verify the thread in which the code is
running. The MessageBox API is used instead of the VB MessageBox command
because the API function is known to be thread safe. A second MessageBox is
shown when the counting is complete, and an event is raised to indicate that
the operation is finished.
The background thread is launched using the following code in the frmMTDemo3
form:
Private Sub cmdCreateFree_Click()
Set c = New clsBackground
StartBackgroundThreadFree c
End Sub
The StartBackgroundThreadFree function is defined in modMTBack
module as follows:
Declare Function CreateThread Lib "kernel32" (ByVal _
lpSecurityAttributes As Long, ByVal dwStackSize As Long, _
ByVal lpStartAddress As Long, ByVal lpParameter As Long, _
ByVal dwCreationFlags As Long, _lpThreadId As Long) _
As Long
Declare Function CloseHandle Lib "kernel32" _
(ByVal hObject As Long) As Long
' Start the background thread for this object
' using the invalid free threading approach.
Public Function StartBackgroundThreadFree(ByVal qobj As _
clsBackground)
Dim threadid As Long
Dim hnd&
Dim threadparam As Long
' Free threaded approach
threadparam = ObjPtr(qobj)
hnd = CreateThread(0, 2000, AddressOf BackgroundFuncFree, _
threadparam, 0, threadid)
If hnd = 0 Then
' Return with zero (error)
Exit Function
End If
' We don't need the thread handle
CloseHandle hnd
StartBackgroundThreadFree = threadid
End Function
The CreateThread function takes six parameters:
lpSecurityAttributes is typically set to zero to use the default security
attributes.
dwStackSize is the size of the stack. Each thread has its own stack.
lpStartAddress is the memory address where the thread starts. This must be
an address of a function in a standard module obtained using the AddressOf
operator.
lpParameter is a long 32 bit parameter that is passed to the function that
starts the new thread.
dwCreationFlags is a 32 bit flag variable that lets you control the start of
the thread (whether it is active, suspended, etc.). Details on these flags
can be found in Microsoft's online 32 bit reference.
lpThreadId is a variable that is loaded with the unique thread identifier of
the new thread.
The function returns a handle to the thread.
In this case we pass a pointer to the clsBackground object that we wish to
use in the new thread. ObjPtr retrieves the value of the interface pointer
in the qobj variable. After the thread is created, the handle is closed
using the CloseHandle function. This does NOT terminate the thread -- the
thread continues to run until the BackgroundFuncFree function exits.
However, if we did not close the handle, the thread object would continue to
exist even after the BackgroundFuncFree function exits. All handles to a
thread must be closed and the thread terminated in order for the system to
free up the resources allocated to the thread.
The BackgroundFuncFree function is as follows:
' A free threaded callback.
' This is an invalid approach, though it works
' in this case.
Public Function BackgroundFuncFree(ByVal param As _
IUnknown) As Long
Dim qobj As clsBackground
Dim res&
' Free threaded approach
Set qobj = param
Do While Not qobj.DoTheCount(100000)
Loop
' qobj.ShowAForm ' Crashes!
' Thread ends on return
End Function
The parameter to this function is a pointer to an interface (ByVal param As
IUnknown). We can get away with this because under COM, every interface is
based on IUnknown -- so this parameter type is valid regardless of the type
of interface originally passed to the function. We must, however,
immediately set the param to a specific object type in order to use it. In
this case qobj is set to the original clsBackground object that was passed
to the StartBackgroundThreadFree object.
The function then enters an infinite loop during which it performs any
desired operation, in this case a repetitive count. A similar approach here
might be to perform a wait operation that suspends the thread until a system
event (such as a process termination) occurs. The thread could then call a
method in the class to signal to the application that the event has
occurred.
Accessing the qobj object is extremely fast because of the free threading
nature of this approach -- no marshalling is used.
You'll notice, however, that if you try to have the clsBackground object
show a form, the application crashes. You'll also notice that the completion
event is never raised in the client form. In fact, even the Microsoft
Systems Journal that describes this approach includes a great many warnings
that there are some things that do not work when you attempt this approach.
Is this a flaw in Visual Basic?
Some people who tried deploying applications using this type of threading
have found that their applications fail after upgrading to VB5 service pack
2.
Does this mean that Microsoft has failed to correctly provide backwards
compatibility?
The answer to both questions is: No.
The problem is not with Microsoft or Visual Basic.
The problem is that the above code is garbage.
The problem is simple -- Visual Basic supports objects in both single
threaded and apartment models. Let me rephrase this: Visual Basic objects
are COM objects that make a statement under the COM contract that they will
work correctly as single threaded or apartment model objects. That means
that each object expects any method calls to take place on the same thread
that created the object.
The example shown above violates this rule.
It violates the COM contract.
What does this mean?
It means that the behavior of the object is subject to change as Visual
Basic is updated.
It means that any attempt of that object to access other objects or forms
may fail disastrously, and that the failure modes may change as those
objects are updated.
It means that even code that works now may suddenly fail as other objects
are added, deleted, or modified.
It means that it is impossible to characterize the behavior of the
application, or to predict whether it will work or should work in any given
environment.
It means that it is impossible to predict whether the code will work on any
given system, and that the behavior may vary depending on the operating
system in use, the number of processors in use, and other system
configuration issue.
You see, once you violate the COM contract, you are no longer protected by
those features in COM that allow objects to successfully communicate with
each other and with clients.
This approach is programming alchemy. It is irresponsible and no programmer
should ever use it. Period.
The CreateThread API Revisited
Now that I've shown you why the CreateThread API approach that has appeared
in some articles is garbage, it's only fair that I make things right and
show you how you can, in fact, use this API safely.
The trick is simple -- you must simply adhere to the COM threading contract.
This takes a bit more work, but the results have proven so far to be
reliable.
The MTDemo3 sample shows this in the frmMTDemo3 form with the following code
that launches an apartment model background class as follows:
Private Sub cmdCreateApt_Click()
Set c = New clsBackground
StartBackgroundThreadApt c
End Sub
So far this looks very similar to the free threading approach. You create
an instance of the class and pass it to a function that starts the
background thread. The following code appears in the modMTBack module:
' Structure to hold IDispatch GUID
Type GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(7) As Byte
End Type
Public IID_IDispatch As GUID
Declare Function CoMarshalInterThreadInterfaceInStream _
Lib "ole32.dll" (riid As GUID, ByVal pUnk As IUnknown, _
ppStm As Long) As Long
Declare Function CoGetInterfaceAndReleaseStream Lib _
"ole32.dll" (ByVal pStm As Long, riid As GUID, _
pUnk As IUnknown) As Long
Declare Function CoInitialize Lib "ole32.dll" _
(ByVal pvReserved As Long) As Long
Declare Sub CoUninitialize Lib "ole32.dll" ()
' Start the background thread for this object
' using the apartment model
' Returns zero on error
Public Function StartBackgroundThreadApt(ByVal qobj As _
clsBackground)
Dim threadid As Long
Dim hnd&, res&
Dim threadparam As Long
Dim tobj As Object
Set tobj = qobj
' Proper marshaled approach
InitializeIID
res = CoMarshalInterThreadInterfaceInStream _
(IID_IDispatch, qobj, threadparam)
If res <> 0 Then
StartBackgroundThreadApt = 0
Exit Function
End If
hnd = CreateThread(0, 2000, AddressOf _
BackgroundFuncApt, threadparam, 0, threadid)
If hnd = 0 Then
' Return with zero (error)
Exit Function
End If
' We don't need the thread handle
CloseHandle hnd
StartBackgroundThreadApt = threadid
End Function
The StartBackgroundThreadApt function is a bit more complex than the free
threading equivalent. The first new function is called InitializeIID. This
function
deals with the following code:
' Initialize the GUID structure
Private Sub InitializeIID()
Static Initialized As Boolean
If Initialized Then Exit Sub
With IID_IDispatch
.Data1 = &H20400
.Data2 = 0
.Data3 = 0
.Data4(0) = &HC0
.Data4(7) = &H46
End With
Initialized = True
End Sub
You see, we're going to need an interface identifier -- a 16 byte structure
that uniquely identifies and interface. In particular, we're going to need
the interface identifier for the IDispatch interface (more information on
IDispatch can be found in my Developing ActiveX Components book). The
InitializeIID function simply initializes the IID_IDispatch structure to the
correctly values for the IDispatch interface identifier. This value is
obtained originally using a registry viewer utility.
Why do we need this identifier?
Because in order to adhere to the COM threading contract, we need to create
a proxy object for the clsBackground object. The proxy object needs to be
passed to the new thread instead of the original object. Calls by the new
thread on the proxy object will be marshaled into the current thread.
The CoMarshalInterThreadInterfaceInStream performs an interesting task. It
collects all of the information needed to create a proxy for a specified
interface and loads it into a stream object. In this example we use the
IDispatch interface because we know that every Visual Basic class supports
IDispatch, and we know that IDispatch marshalling support is built into
Windows -- so this code will always work. We then pass the stream object to
the new thread. This object is designed by Windows to be transferable
between threads in exactly this manner, so we can pass it safely to the
CreateThread function. The rest of the StartBackgroundThreadApt function is
identical to the StartBackgroundThreadFree function.
The BackgroundFuncApt function is also more complex than the free threaded
equivalent as shown below:
' A correctly marshaled apartment model callback.
' This is the correct approach, though slower.
Public Function BackgroundFuncApt(ByVal param As Long) _
As Long
Dim qobj As Object
Dim qobj2 As clsBackground
Dim res&
' This new thread is a new apartment, we must
' initialize OLE for this apartment
' (VB doesn't seem to do it)
res = CoInitialize(0)
' Proper apartment modeled approach
res = CoGetInterfaceAndReleaseStream(param,
IID_IDispatch, qobj)
Set qobj2 = qobj
Do While Not qobj2.DoTheCount(10000)
Loop
qobj2.ShowAForm
' Alternatively, you can put a wait function here,
' then call the qobj function when the wait is satisfied
' All calls to CoInitialize must be balanced
CoUninitialize
End Function
The first step is to initialize the OLE subsystem for the new thread. This
is necessary for the marshalling code to work correctly. The
CoGetInterfaceAndReleaseStream creates the proxy object for the original
clsBackground object and releases the stream object used to transfer the
data from the other thread. The IDispatch interface for the new object is
loaded into the qobj variable. It is now possible to obtain other
interfaces -- the proxy object will correctly marshal data for every
interface that it can support.
Now you can see why the loop is placed in this function instead of in the
object itself. When you call the qobj2.DoTheCount function for the first
time, you'll see that the code is running in the original thread! Every time
you call a method on the object, you are actually calling the method on the
proxy object. Your current thread is suspended, the method request is
marshaled to the original thread, and the method called on the original
object in the same thread that created the object. If the loop was in the
object, you would be freezing up the original thread.
The nice thing about this approach is that everything works. The
clsBackground object can show forms and raise events safely. Of course it
can -- it's running in the same thread as the form and its client -- as it
should be. The disadvantage of this approach is, of course, that it is slow.
Thread switches and marshalling are relatively slow operations. You would
never actually want to implement a background operation as shown here.
But this approach can work extremely well if you can place the background
operation in the BackgroundFuncApt function itself! For example: you could
have the background thread perform a background calculation or a system wait
operation. When it is complete, it can call a method on the object which
will raise an event in the client. By keeping the number of method calls
small relative to the amount of work being done in the background function,
you can achieve very effective results.
What if you want to perform a background operation that does not need to use
an object? Obviously, the problems with the COM threading contract vanish.
But other problems appear. How will the background thread signal completion
to the foreground thread? How will they exchange data? How will the two
threads be synchronized? All of these things are possible with appropriate
use of API calls. Refer to my Visual Basic 5.0 Programmer's Guide to the
Win32 API for information on synchronization objects such as Events,
Mutexes, Semaphores and Waitable Timers.
It also includes examples of memory mapped files which can be helpful in
exchanging data between processes. You may be able to use global variables
to exchange data as well -- but be aware that this behavior is not
guaranteed by Visual Basic (in other words, even if it works now, there is
no assurance that it will work in the future). I would encourage you to use
API based techniques to exchange data in this case. However, the advantage
of the object based approach shown here is that it makes the problem of
exchanging data between threads trivial -- simply do it through the object.
Conclusion #1
I once heard from an experienced Windows programmer that OLE is the hardest
technology he's ever needed to learn. I agree. It is a vast subject and
parts of it are very difficult to understand. Visual Basic, as always, hides
much of the complexity from you.
There is a strong temptation to take advantage of advanced techniques such
as multithreading using a "tips and techniques" approach. This temptation is
encouraged by articles that sometimes present a particular solution,
inviting you to cut and past their techniques into your own applications.
When I wrote my original Visual Basic Programmer's Guide to the Windows API,
I explicitly disavowed that approach. I felt that it is generally
irresponsible to include code in an application that you don't understand,
and that real knowledge, while hard to gain, is worth the effort even in the
short run.
Thus my API books were designed not to provide quick answers and easy
solutions, but to teach API usage to such a degree that programmers can
intelligently apply even the most advanced techniques correctly, quickly
going beyond what is shown in the book. I applied this same approach to my
book on Developing ActiveX Components, which spends a great deal of time
discussing the principles of ActiveX, COM and object oriented programming
before getting into implementation details.
Much of my career in the Visual Basic field, and much of Desaware's
business, has been based on teaching Visual Basic programmers advanced
techniques. The reader who inspired this article by criticizing me of
holding back on threading technology and thus betraying this principle
missed the point.
Yes, I teach and demonstrate advanced techniques -- but I try never to miss
the bigger picture. The advanced techniques that I teach must be consistent
with the rules and specifications of Windows. They must be as safe to use as
possible. They must be supportable in the long run. They must not break when
Windows or Visual Basic changes.
I can claim only partial success -- it's a hard line to draw sometimes, and
Microsoft is at liberty to change the rules whenever they wish. But I always
keep it in mind and try to warn people where I think I may be pushing the
limit.
I hope this multithreading discussion shown here demonstrates the dangers of
applying "simple techniques" without a good understanding of the underlying
technology.
I can't promise that the apartment model version of CreateThread usage is
absolutely correct -- only that it is safe to the best of my understanding
and testing.
There may be other factors that I have missed -- OLE is indeed complex and
both the OLE DLLs and Visual Basic itself keep changing. I can only say that
to the best of my knowledge, the code I've shown does obey the COM rules and
that empirical evidence shows that Visual Basic 5.0's runtime is
sufficiently thread safe to run background thread code in a standard module.
Sequel #1- Regarding VB6
The following comments were written after the release of VB6
Sigh... It seems that many readers missed my original point. The ideas was
not to encourage VB programmers to use CreateThread with Visual Basic. It
was to explain clearly and accurately why you shouldn't use CreateThread
with Visual Basic.
So, when Visual Basic 6 turned out to be considerably less thread-safe than
VB5, breaking the sample programs referenced by this article, what could I
do? I suppose I could go back and revise the samples and try to make them
work with VB6. But then the same problem might arise with later versions of
Visual Basic as well.
Visual Basic offers good support of multithreading including multithreaded
clients in ActiveX servers (this is described quite thoroughly in the latest
edition of my Developing COM/ActiveX components book). I strongly encourage
you to stay within the rules defined by the Visual Basic documentation and
not use the CreateThread API with Visual Basic.
For those who insist on pursuing CreateThread further, to start with you
should eliminate all Declare statements and use a type library instead. I
don't promise that this will fix the problem, but my initial testing
indicates that it is a necessary first step.
Sequel #2 - Regarding SpyWorks 6.2
April 2000...
It seems that telling people not to use CreateThread wasn't a satisfactory
answer after all. I continued to receive requests for information on how to
create threads both for background operations and to use NT synchronization
objects from VB DLL's. As you probably know, when enough people ask for
something that isn't easy or possible to do with Visual Basic, sooner or
later it shows up as a new feature in SpyWorks. With version 6.2, we've
included a component called dwBackThread that allows you to create objects
from your VB DLL in their own thread and then trigger background operations.
The component handles all of the necessary marshaling and cleanup for you so
that it's as safe as one can be when doing multithreading. Most important -
it follows all of the COM threading rules, so you don't have to worry about
pieces of VB or components you use suddenly failing to work. See our product
pages on SpyWorks for further details.
For further reference:
"Dan Appleman's Developing COM/ActiveX Components with Visual Basic 6.0: A
Guide to the Perplexed" published by SAMS, ISBN 1-56276-576-0.
"Dan Appleman's Visual Basic 6.0 Programmer's Guide to the Win32 API"
published by SAMS, ISBN 0-672-31590-4.
Desaware's web site at http://www.desaware.com/.