|Products Purchase Publishing Articles Support Company Contact|
Articles > COM > A Thread to Visual Basic
Copyright © 1997-2004 by Daniel Appleman -- All rights reserved.
This article may not be reprinted or distributed electronically or in any other form. Access to this article may be obtained at no cost by accessing our Technical Articles page. Web sites are invited to link directly to the above URL to provide access to this article.
This article is presented in its original form, with "updates" appended to illustrate changes as technology evolved. Occasionally someone contacts us to find out why the sample code doesn't work with VB6. The reason is, as you will soon see, that the example code was a theoretical exercise for VB5. It's a great demonstration of why you should NEVER call the CreateThread API from Visual Basic (any version including VB .NET). If you are looking for background threading in Visual Basic, this article will discuss several safe and reliable approaches for doing so. The CreateThread API is not one of them.
You can download the sample code from ftp.desaware.com/SampleCode/Articles/Thread.zip
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.
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:
Or it could be:
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?
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.
There are two relatively easy ways to avoid multithreading problems.
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.
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
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
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
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 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.
Multithreading Client -- Implemented as an ActiveX EXE Server
Multithreaded Servers DLL or EXE
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).
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:
The Apartment Threading Model
The Free 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.
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
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:
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.
Some people who tried deploying applications using this type of threading have found that their applications fail after upgrading to VB5 service pack 2.
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?
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.
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
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.
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.
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.
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.
Visual Basic .NET and C# both use the .NET framework, which includes classes to create and manage threads (the System.Threading namespace). This eliminates the need to use the CreateThread API. In fact, it is critical that you not use the CreateThread API in .NET because .NET threads do not necessarily have a one to one correspondence with system threads - especially in hosted environments.
"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.
"Moving to VB .NET: Strategies, Concepts and Code" published by Apress, ISBN 1-59059-102-X
Desaware's web site at www.desaware.com.
For notification when new articles are available, sign up for Desaware's Newsletter.
|Products Purchase Articles Support Company Contact