Multithreaded connectable objects may be the cause of
several problems to clients incapable of multithreaded operations. This paper
examines how to overcome those problems when proper inter-thread marshalling
may not be used
Using Multithreaded
Connectable Objects in VB6
Nicola Di Nisio(*)
Connectable
objects are COM objects that are able to perform callbacks towards their
clients, via known outgoing
interfaces. Callback interfaces are said outgoing interfaces because they come
out from the clients of an object and are used by the object, just the opposite
of the usual relation between a client and the server object:

In the
picture above we can see the relation between an object MtCom and its client.
The client uses the object via its interface IMtCom, but at the same time the
client implements the interface _IMtCom, specified by the type library of the
component MtCom, and gives a pointer to that interface to the object.
_IMtCom is
an outgoing interface, a callback interface for the object to the client.
In the
Visual Basic terminology the methods of the interface _IMtCom are said to be events of the objects. Lightnings in the
Visual Basic object browser represent the events of an object.
An object
with events should be declared by using the keyword WithEvents and instantiated later by means of CreateObject(), or the New
operator. Here is an example of how a client VB application should declare and
instantiate the MtCom object:
Dim WithEvents
MtCom As MtCom
Private Sub
Form_Load()
Set MtCom =
CreateObject("DNN.MtCom")
End Sub
Each time
the object MtCom fires an event a handling subroutine is executed. The name of
that handling routine is the juxtaposition of the name of the firing object and
the name of the fired event, separated by an underscore.
VB6 builds
the outgoing interface on the fly and in correspondence of an event firing, it
searches for the proper handling routine to execute it. If it does not find, it
raises an access violation.
The check
for the existence of all the expected handling routines is not performed at
compile-time, hence be careful when typing the name of the handling routines.
Alternatively let the VB IDE generate them, by selecting them on the right
combo-box on top of the editor, where you can find the list of events of the
object selected in the left combo-box.
If MtCom is
an in-proc component, it is created in the same thread of the creator, thus if
MtCom is single threaded it may not cause any race condition. This assertion
remains valid even if it is an out-of-proc component, until it is single
threaded, because in COM all method calls are synchronous, that is the caller
thread is blocked until the method call returns.
The
observations above lead to a minor issue shown in the following fragment of VB
code. Suppose that IMtCom has a method DoSomething()
and that there is an event OnDoSomething()
in the outgoing interface _IMtCom to report progress information for DoSomething(), with the following signature
OnDoSomething(Progress
As Long, Message As String)
The event
returns a Progress information, and a
string message. We may think to use the progress information to update a progress
bar and to show the message in a list box, like in the following code
Private Sub
MtCom_OnDoSomething(ByVal Progress As Long, ByVal Message As String)
ProgressBar1.Value = Progress
List1.AddItem Message
End Sub
Suppose
that the object MtCom, when we call DoSomething(),
fires three times that event, with progress information equal to 1, 2 and 3 and
the corresponding messages are “1”, “2” and “3”. Suppose also that those events
are fired at a rate of one per second. Well, you will see all those information
in your listbox only after the last event is fired, after three seconds, and
you will see your progress bar stay still for three seconds and only after the
third event going to 100%, without intermediate progresses. This happened
because the form and its components were not refreshed so you didn’t see
anything. They were not refreshed, because the main thread of the application
was blocked on the IMtCom::DoSomething()
method call.
To overcome
this trouble you have to explicitly refresh the visual components during the
update of their data
Private Sub
MtCom_OnDoSomething(ByVal Progress As Long, ByVal Message As String)
ProgressBar1.Value = Progress
List1.AddItem Message
ProgressBar1.Refresh
List1.Refresh
End Sub
Multithreaded
connectable objects propose another scenario, at least by their asynchronous
methods.
In COM all
method calls are synchronous, they block the calling thread until they return.
You may implement asynchronous method call, by letting the method start another
thread that performs the operations, in order to return the control to the
caller before the completion of the operations in the second thread. In COM+ we
will be able to implement an asynchronous method in a declarative fashion, without
explicitly start another thread.
Suppose to
have a DoSomethingAsych() method in
IMtCom that performs the same operations of DoSomething(),
but asynchronously, then we may think that the refresh problem on the
components of the form should no more exist. In fact the main thread of the
application is not blocked on the call and it may freely repaint the form and
its components while the events coming from MtCom add new messages and progress
information on our form. Well our picture is still not so good, because another
trouble happens. A COM golden rule is that it is not allowed for unmarshalled
interfaces to be passed across thread boundaries. In our case this means that
the _ImtCom interface must be marshalled inside each thread firing events of
this interface, otherwise access violations could arise, or even worse, race
conditions. In fact this rule exists to protect clients uncapable to perform
multi-threaded operations and clients compiled from VB programs are among them.
To do this
could be simple, tough boring, when we code all component by hand in C++, but
it could become impractical when we use ATL 3.0 to code our multithreaded
connectable object. In fact when the client registers himself to the server
calls IconnectionPoint::Advise() in its thread of execution. There a
pointer to the outgoing interface is stored into m_vec and from that same array is retrieved and
used in each Fire_ method. In this case you should marshall the incoming
interface pointers in Advise() and then unmarshall them in each Fire_
method. This requires modifications to the wizard-generated code in the Fire_
methods (a wizard-generated comments reminds you that any change to that code
may be lost in future regeneration of the same code, due for example to
interface change…) and worse changes to the ATL code (this same topic is
discussed in the paper Dr. GUI and COM Events, Part 2, at http://msdn.microsoft.com/).
Until a new
version of ATL will solve this problem, we have two choices
The first
solution is obvious and tedious, but works fine. It is the most cost-effective when
you have many clients to code and what to keep them simple.
The second
solution might be more practical, but it is dangerous. You have to modify the
ATL source installed for you by Visual Studio and this may affect other
components you maintain with that installation of Visual Studio. I do not like
this, especially in a production environment.
The last
solution is not a solution, but a workaround, that is cost-effective only when
you have few clients to code that use your multithreaded object. We are going
to analyze this workaround.
This is a
quick-and-dirty answer to the multi-thread issue.
When an
event takes some information that have to be added in thread-unsafe structure
shared with a controlling thread, we have to queue those information somewhere
an let the controlling thread add those information for us. This is the case of
our listbox, let’s take a look to the solution. First of all we need of a
thread-safe queue and for the purpose of this article a thread-safe queue of variants
was developed. It has two methods:
Sub Add(ByVal
v As Variant)
Function
Pop(ByRef v As Variant) As Long
The first
method adds a variant value to the queue, while the second pops the older value
in the queue and return True if there
is still another item in the queue, False
otherwise.
The PROGID
of this queue is DNN.TSVariantQueue and implemented by the TSVariantQueue.dll,
which accompanies this paper.
The item
queued are read by the handler of a timer running in the form, hence running
under the controlling thread of the components on the form, and from that
handler are placed in the proper component. The timer interval should be small
enough for us not to register delays, but not smaller, to avoid overheads.
Status
information, instead, do not need to be queued, they can be simply stored in
private attribute of the form and read
in by the handle routine of the timer. An example of such information is the
progress value.
Supposing
to have an event OnDoSomethingAsynch()
fired by the method DoSomethingAsynch() and
a timer named Timer1, with the relative handler routine, the whole code to
handle our events coming from an asynchronous method should look like the
following:
Dim WithEvents
MtCom As MtCom
Dim
TsqMessages As New TSVariantQueue
Private ProgressAsynch
As Integer
Private Sub
Form_Load()
Set MtCom =
CreateObject("DNN.MtCom")
ProgressAsynch = 0
End Sub
Private Sub
MtCom_OnDoSomethingAsynch(ByVal Progress As Long, ByVal Message As String)
ProgressAsynch = Progress
TsqMessages.Add Message
End Sub
Private Sub
Timer1_Timer()
Dim Message As Variant
ProgressBar1.Value = ProgressAsynch
While TsqMessages.Pop(Message)
List1.AddItem Message
Wend
End Sub
To conclude
let’s run this all on real code. Two COM components and a VB6 client
application accompany this paper.
The first
component is the aforementioned thread safe queue TSVariantQueue, in the
TSVariantQueue.dll.
The second
component is the component MtCom component shown at the beginning of the paper,
implementing the interface IMtCom and defining the outgoing interface for its
client _IMtCom. The MtCom.dll implements it.
You must
register those components to use them by executing the following command in a
command shell open on the directory where you placed the dll
RegSvr32
TSVariantQueue.dll
RegSvr32
MtCom.dll
Here is the
VB-like complete definition of the interfaces IMtCom and _IMtCom:
IMtCom
Sub DoSomething()
Sub DoSomethingAsynch()
_IMtCom
OnDoSomething(Progress As Long, Message As
String)
OnDoSomethingAsynch(Progress As Long,
Message As String)
DoSomething() is a synchronous method that fires three times
the event OnDoSomething() with
varying values for Progress (1, 2 and
3) and Message (“1”, “2” and “3”).
The events are fired at a rate of one per second. DoSomethingAsynch() does the same but asynchronously.
The visual
basic application’s form looks like the following:

The button
“DoSomething” calls the method DoSomething(),
while the button “DoSomethingAsynch” calls the DoSomethingAsynch(). At the center of the form there is the listbox
where the messages are written into and at the bottom there is the progress
bar.
By the
check box on the top right you can choose whether or not to use the queue.
There is a timer on the form, with 200 ms of timer interval named Timer1 that reads the queue and writes
messages and progress information when you choose to use the queue.
Here is the
code of this Visual Basic application:
Option
Explicit
Dim WithEvents
MtCom As MtCom
Dim
TsqMessages As New TSVariantQueue
Private
ProgressAsynch As Integer
Private Sub
DoSomethingCommand_Click()
List1.Clear
ProgressAsynch = 0
ProgressBar1.Value = 0
ProgressBar1.Refresh
MtCom.DoSomething
End Sub
Private Sub
DoSomethingAsynchCommand_Click()
List1.Clear
ProgressAsynch = 0
ProgressBar1.Value = 0
MtCom.DoSomethingAsynch
End Sub
Private Sub
Form_Load()
Set MtCom =
CreateObject("DNN.MtCom")
ProgressAsynch = 0
End Sub
Private Sub
MtCom_OnDoSomething(ByVal Progress As Long, ByVal Message As String)
If (1 = UseQueueCheck.Value) Then
ProgressAsynch = Progress
TsqMessages.Add Message
Else
ProgressBar1.Value = Progress
List1.AddItem Message
ProgressBar1.Refresh
List1.Refresh
End If
End Sub
Private Sub
MtCom_OnDoSomethingAsynch(ByVal Progress As Long, ByVal Message As String)
If (1 = UseQueueCheck.Value) Then
ProgressAsynch = Progress
TsqMessages.Add Message
Else
ProgressBar1.Value = Progress
List1.AddItem Message
End If
End Sub
Private Sub
Timer1_Timer()
Dim Message As Variant
If (ProgressAsynch <> 0) Then
ProgressBar1.Value = ProgressAsynch
End If
While TsqMessages.Pop(Message)
List1.AddItem Message
Wend
End Sub
As you can
see the check box is thread safe as regard the reading of its value. Usually
most of the operations you do with components into your event handler routines may
lead to errors, because of interactions with their controlling thread, and also
other apparently harmless operations, like the string concatenation (e.g. “aa”
& “bb”), are source of trouble. Only the experience may guide you in what
you can and cannot do in you event handler routines for events fired by
asynchronous methods.
Here is the
matrix of errors or problems for that application. The access violation comes
out only in the executable version of the application, not when running in the
VB environment:
|
|
Use Queue |
Do Not Use Queue |
DoSomething
|
Lack
of refresh |
Ok (with
forced Refresh) |
|
DoSomethingAsynch |
Ok |
Access
Violation |
The “Lack
of refresh” for the DoSomething() method
when using the queue is due to the fact that the queue is read by the timer,
the timer is controlled by the main thread of the application and this thread
is blocked on the synchronous call to DoSomething().
Only at the return from DoSomething() the
timer can read the queue and the last progress value: all the three messages
are written together and the progress bar goes from 0 to 3 in a single leap.
VB6 is not a
completely thread safe environment, some parts of it are not. Furthermore the
components we use in our VB applications may not be thread safe, both the ones
shipped with VB and third party ones.
Errors may
arise when we use those thread-unsafe parts in events handler for events fired
by asynchronous methods, when they are not meant to deal with thread-unsafe
clients. We have to test all such event handlers against such trouble and this
test is to be performed out of the VB environment, because at least in VB6 they
usually do not come out when we run the application in the VB IDE. Run the
executable instead.
When we
discover components, or the part of them, that are not thread-safe, we have to
use a timer to let the controlling thread update the interested properties or
call the interested methods. Private attributes have to be used to store status
information and thread-safe queues to store sequences of messages.
The timer
interval should be smaller enough to let not perceive any delay in the
information flow, but not smaller, in order to avoid overheads.
The
problems investigated in this paper may appear in any environment using
multithreaded (and not properly coded) connectable objects, not only VB, and
the solution here exposed may be used in all such situations, being based on a
queue that is a COM object and on a timer.
(*) The author has got a degree in Computer
Science and is a Microsoft Certified Professional. Works as software developer
for Dataspazio S.p.A. in Roma, Italy. His homepage is at www.dinisio.net/nicola