CSDN博客

img sonicdater

A Thread to Visual Basic

发表于2002/3/11 9:16:00  1270人阅读

分类: VB_Thread

A Thread to Visual Basic

This article discusses multithreading issues with Visual Basic.
 

Copyright © 1997-2000 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.

Contents:

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.

  1. Avoid all use of global variables.

  2. 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 www.desaware.com.

0 0

相关博文

我的热门文章

img
取 消
img