|Products Purchase Publishing Articles Support Company Contact|
Articles > COM > Waiting with Threads
Copyright © 2001 by Daniel Appleman -- All rights reserved.
One of the most intriguing features of C++ is its ability to launch background threads to wait efficiently for system events. This capability is incorporated into VB.Net, but many developers are still using VB6 and in many cases it will not make sense to port existing code to the new environment. For today's applications, and maintaining applications in VB5 and VB6, the SpyWorks background thread component provides a powerful mechanism for using background threads to perform highly efficient wait operations on Win32 synchronization objects.
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.
There are many situations where your application needs to wait for something to occur. In many cases the mechanism for waiting is built into the language or operating system. For example: your application might wait for the user to click on a button, or wait for the user to enter text. When an application waits for the user to do something, your code is effectively idle (i.e., does not use CPU time), and your code does not start running until the appropriate event is raised.
How does VB detect a button click?
It could continuously watch the mouse location and mouse buttons. When the mouse button is clicked over the location of the button on the form, the software would raise the click event. This approach is called "polling". Polling is the least efficient way to wait for something to occur - because the processor must continuously test for the wait condition to be satisfied.
What actually happens with Visual Basic is that your application thread is suspended until the operating system detects the user action. At that point, a message is placed on the thread's message queue, and the thread "wakes up". Visual Basic processes the message and raises the appropriate events in your code. The ability to suspend a thread until something happens is essential to maintaining high performance in an operating system.
Windows defines many situations where it is possible to wait for "something" to happen. For example:
In each of these cases (and others not mentioned here), the "something" you are waiting for is defined by an element called a synchronization object. In a somewhat recursive definition, a synchronization object is any object which the operating system can wait on using one of the Win32 API wait functions. Each of these synchronization objects has two states: signaled and unsignaled (though some, such as a mutex, can have an additional state indicating it has been abandoned). For example: A process object is signaled once the process terminates.
A list of synchronization objects, along with the API calls needed to use them, can be found in my "Visual Basic Programmer's Guide to the Win32 API".
In Visual Basic, you might have one application wait for a different process to terminate using code like this:
Do DoEvents result = WaitForSingleObject(hProcess, 50) Loop While result<>WAIT_TIMEOUT
The WaitForSingleObject function suspends the thread, then returns when the process terminates or when 50 milliseconds elapses, whichever comes first. Why does this code use a loop?
If the WaitForSingleObject function set an infinite timeout (a timeout of -1), the thread would remain suspended until the other process terminated. Unfortunately, suspending the thread would completely freeze the application - since no events can execute while the thread is suspended. Wait operations are usually placed in threads that are created solely for the purpose of waiting for a synchronization object to be signaled. Unfortunately, Visual Basic 6 does not allow you create a background thread for this purpose.
Visual Basic.Net will allow VB programmers to create threads for waiting (and other purposes), but not only is .Net not shipping at this time, but the transition to .Net is likely to be a long one (as will be discussed in my forthcoming book "Moving to VB.Net: Strategies, Concepts and Code").
Fortunately, SpyWorks includes a background thread component that is ideal for this purpose.
You can download the sample code for this article at ftp://ftp.desaware.com/SampleCode/Articles/waitfor.zip. There are two sample programs in this file, waitfor and waitfor2. There is also a demo version of the Desaware Spyworks background thread component dwBkDemo.dll. Read the section on installation at the end of this article for further details on installing the demo component and testing the examples..
Let's begin with the Waitfor.vbp example.
The WaitFor project defines a DLL that performs a function and waits on a synchronization using a background thread. It contains two classes. The Main class is the one that performs the main tasks of the DLL. All API declarations are in the modWaitFor.bas module and are not shown in the article. The background class, named WaitThreadClass, is shown here:
' WaitFor example - background thread class ' Copyright ©2001 by Desaware Inc. All Rights Reserved Option Explicit Event WaitComplete() Private m_ObjectToWaitFor As Long Private m_TerminateEvent As Long Public Sub SetParameters(ByVal ObjectToWaitFor As Long, ByVal _ TerminateEvent As Long) m_ObjectToWaitFor = ObjectToWaitFor m_TerminateEvent = TerminateEvent End Sub Public Sub ExecuteBackground() Dim ObjectArray(1) As Long ObjectArray(0) = m_TerminateEvent ObjectArray(1) = m_ObjectToWaitFor ' Wait infinite until object is signaled, or event raised Call WaitForMultipleObjects(2, ObjectArray(0), False, -1) RaiseEvent WaitComplete End Sub
This class defines an object that is created by SpyWorks on its own thread. All of the method and property calls made to this object are correctly marshaled to the object's thread (thus following the apartment model threading rules required by a VB program - see the article "A Thread to Visual Basic" at - the article also includes an in depth introduction to multithreading for those who are having trouble following this article).
The SetParameters method is used to pass parameters to the background thread. You can define as many methods or properties as you wish to set the object up for the background operation. The ExecuteBackground method is called asynchronously by the background thread component. Because it is called on its own thread, that thread can be suspended safely without interfering with the operation of the rest of the DLL or any applications that are using that DLL.
The ExecuteBackground method uses the WaitForMultipleObjects function to wait on two objects. The first is the object that your application wants to wait for. The other is an Event object that is used to exit the wait operation reactivate the thread in the case where the application is ending, or the caller wishes to programmatically abort the wait operation. Note that an Event object is a type of synchronization object and has nothing to do with VB events.
The MainClass class in the DLL manages the background thread and performs a wait operation on an object passed by a calling application. I must stress here that this class (and DLL) can expose additional methods and properties to perform completely unrelated tasks. It is an ActiveX DLL like any other.
The m_WaitThreadClass member contains a reference to the background class object, and will receive event notifications when the wait condition is satisfied. The BackObjControl object is the object from the Desaware Background Thread Component that creates and manages the actual thread for the background class. The m_WaitEvent member contains an Event object that, when signaled, will notify the background object that it must stop waiting.
' WaitFor example - main class ' Copyright ©2001 by Desaware Inc. All Rights Reserved Option Explicit Event WaitComplete() Private m_WaitEvent As Long ' Event to force termination Private WithEvents m_WaitThreadClass As WaitThreadClass Private Terminating As Boolean Private BackObjControl As New dwObjLaunch ' This function can easily be extended to wait on multiple objects Public Sub WaitForThisObject(ByVal obj As Long) If Not m_WaitThreadClass Is Nothing Then ' If you're currently waiting, terminate that wait AbortWait End If ' Launch the new background thread Set m_WaitThreadClass = BackObjControl.LaunchObject _ ("WaitFor.WaitThreadClass") Call m_WaitThreadClass.SetParameters(obj, m_WaitEvent) Call ResetEvent(m_WaitEvent) BackObjControl.BackgroundExecute End Sub Public Sub AbortWait() Call SetEvent(m_WaitEvent) Do ' Wait for termination DoEvents ' Event will show up during a DoEvents Loop While Not m_WaitThreadClass Is Nothing End Sub Private Sub Class_Initialize() m_WaitEvent = CreateEvent(0, True, 0, vbNullString) End Sub Private Sub Class_Terminate() Terminating = True AbortWait End Sub Private Sub m_WaitThreadClass_WaitComplete() If Not Terminating Then ' Don't try to raise an event while terminating RaiseEvent WaitComplete End If CloseHandle m_WaitEvent Set m_WaitThreadClass = Nothing Set BackObjControl = Nothing End Sub
When the class terminate event occurs, or the AbortWait method is called, the m_WaitEvent Event is signaled. The routine then waits until WaitComplete event is raised and the background object terminated.
The test application allows you to experiment with the WaitFor DLL. It has three command buttons, one to start a wait, another to abort it, and a third to signal the object that you are waiting for.
' WaitForTest background thread test program ' Copyright ©2001 by Desaware Inc. All Rights Reserved Option Explicit Private WithEvents m_WaitFor As WaitFor.MainClass Private m_Event As Long Private Sub cmdAbort_Click() Call m_WaitFor.AbortWait End Sub Private Sub cmdSignal_Click() SetEvent m_Event End Sub Private Sub cmdStart_Click() ResetEvent m_Event ' Make sure event isn't already signaled Call m_WaitFor.WaitForThisObject(m_Event) cmdStart.Enabled = False cmdAbort.Enabled = True End Sub Private Sub Form_Load() ' m_Event represents any synchronization object m_Event = CreateEvent(0, 1, 0, txtEvent.Text) Set m_WaitFor = New WaitFor.MainClass End Sub Private Sub Form_Unload(Cancel As Integer) ' Be sure to close your synchronization objects CloseHandle m_Event End Sub Private Sub m_WaitFor_WaitComplete() MsgBox "Event signaled" ResetEvent m_Event cmdStart.Enabled = True cmdAbort.Enabled = False End Sub
Here are two things to try:
The Event object used in the WaitForTest application represents any synchronization object. Because it uses a named Event object, the object can be accessed from any application.
The WaitFor example satisfies the initial requirement to create a background thread that can perform a highly efficient wait operation. However, it's design leaves a little bit to be desired.
The WaitFor2 example addresses both of these issues. The code for the WaitThreadClass2 background class is shown here:
' WaitFor2 example - background thread class ' Copyright ©2001 by Desaware Inc. All Rights Reserved Option Explicit Event WaitComplete() Private m_ObjectToWaitFor As Long Private m_WaitEvent As Long ' Event to force termination Private m_Waiting As Boolean Public Sub Abort() Call SetEvent(m_WaitEvent) End Sub Public Property Get Waiting() As Boolean Waiting = m_Waiting End Property Public Sub SetParameters(ByVal ObjectToWaitFor As Long) m_ObjectToWaitFor = ObjectToWaitFor End Sub Public Sub ExecuteBackground() Dim ObjectArray(1) As Long Dim res As Long m_Waiting = True ObjectArray(0) = m_WaitEvent ObjectArray(1) = m_ObjectToWaitFor Call ResetEvent(m_WaitEvent) ' Wait infinite until object is signaled, or event raised Do ' Note that MsgWaitForMultipleObjects breaks to ' process messages, including the marshaling ' messages needed to invoke methods on this object res = MsgWaitForMultipleObjects(2, ObjectArray(0), _ False, -1, &HFF&) If res > 1 Then DoEvents Loop While res > 1 RaiseEvent WaitComplete m_Waiting = False End Sub Private Sub Class_Initialize() m_WaitEvent = CreateEvent(0, True, 0, vbNullString) End Sub Private Sub Class_Terminate() CloseHandle m_WaitEvent End Sub
The first thing you may notice is that the Event object used to abort the wait operation is now private to the background class. This eliminates the need for the main class to create the event, pass it as a parameter to the SetParameters function, and delete it afterwards. The background class itself exposes an Abort method to abort the current wait operation. It also includes a "Waiting" property to determine if a wait operation is currently in progress.
Maintaining the Event object within the class and exposing the Abort method from the class requires that there be a way to call methods on this class even when the thread is suspended. How is this possible in an apartment model component?
To understand how this works, consider what it means to have an apartment model component - one in which all method and property calls for the object occur on the same thread. It means that any time a different thread wishes to call a method on that object, the call must be marshaled between threads. It turns out that this marshaling is accomplished by sending messages between the threads. VB creates hidden windows whose sole task is to receive marshaling messages and invoke methods or access properties on objects.
So, all we need to do to allow methods to be called while waiting is to somehow detect when a message is arriving on the thread and perform a DoEvents call. During the DoEvents call, the method will be invoked.
Fortunately, the Win32 API provides a function, MsgWaitForMultipleObjects, that terminates the wait operation when any message needs to be processed by the thread (you can also specify a subset of messages to process). If the result of the MsgWaitForMultipleObjects call is larger than the number of objects minus one, the wait operation terminated due to a message. So you need simply call DoEvents, then call the wait function again.
This approach simplifies the MainClass object considerably as shown here:
' WaitFor2 example - main class ' Copyright ©2001 by Desaware Inc. All Rights Reserved Option Explicit Event WaitComplete() Private WithEvents m_WaitThreadClass As WaitThreadClass2 Private Terminating As Boolean Private BackObjControl As New dwObjLaunch ' This function can easily be extended to wait on multiple objects Public Sub WaitForThisObject(ByVal obj As Long) If m_WaitThreadClass Is Nothing Then Set m_WaitThreadClass = BackObjControl.LaunchObject _ ("WaitFor2.WaitThreadClass2") Else ' If you're currently waiting, terminate that wait AbortWait End If ' Launch the new background thread Call m_WaitThreadClass.SetParameters(obj) BackObjControl.BackgroundExecute End Sub Public Sub AbortWait() If m_WaitThreadClass Is Nothing Then Exit Sub m_WaitThreadClass.Abort Do DoEvents Loop While m_WaitThreadClass.Waiting End Sub Private Sub Class_Terminate() Terminating = True AbortWait Set m_WaitThreadClass = Nothing Set BackObjControl = Nothing End Sub Private Sub m_WaitThreadClass_WaitComplete() If Not Terminating Then ' Don't try to raise an event while terminating RaiseEvent WaitComplete End If End Sub
The architecture of this class has been changed slightly so that instead of creating and terminating the background object (and its thread) every time a wait operation needs to be performed, the object (and its background thread) sits idle until it is called. The Desaware background thread component uses similar techniques internally to keep the background thread in an efficient wait state until it is needed by the calling application.
You can test the WaitFor example using the WaitForTest2 sample program that is virtually identical to the WaitForTest example.
When testing DLL's that use the Desaware background thread component, it is critical to test with the compiled applications. The VB environment is single threaded, which means that while the background thread component will create a new thread, VB will marshal all calls back to the original environment thread even though the object was created by a background thread. This means that the behavior of a DLL tested in the environment will not reflect the actual behavior of the compiled application. It is also far easier to enter deadlock states when testing in the environment.
If you use wait functions with the background thread component (or in any other situation), it is important to incorporate a mechanism to abort the wait before the application terminates. Failing to do so can prevent the application from terminating properly (or at all). With careful design it is possible to use the Desaware background thread component to dramatically reduce the impact on the system of VB applications that have to wait on system events. Because the architecture used by code that uses this component will port easily to VB.Net in the future, this approach is ideal for current applications, and can be easily incorporated into VB.Net projects.
An in depth discussion of the different types of synchronization objects and how to use them from Visual Basic can be found in my "Visual Basic Programmer's Guide to the Win32 API". The book demonstrates using the synchronization objects only in polled applications since the SpyWorks background thread component did not exist at the time the book was published.
For an in depth discussion of threading, including the different threading models, refer to my article "A Thread to Visual Basic".
Sample code for this application can be found at ftp://ftp.desaware.com/SampleCode/Articles/waitfor.zip. There are two sample programs in this file, waitfor and waitfor2. There is also a demo version of the Desaware Spyworks background thread component dwBkDemo.dll. To test the examples, install the background thread component and register it with the line "Regsvr32 dwBkDemo.dll". Then register waitfor.dll and waitfor2.dll in the same manner (they are VB6 applications. VB6 must be installed on your system to run these examples. The dwBkDemo.dll demo component can only be used with the WaitForTest and WaitForTest2 applications - you will receive an object creation error if you try to use it with other applications. Owners of SpyWorks professional can use the full component by changing the project references for the WaitFor and WaitFor2 examples from the demo component to the full version.
Further information on Desaware's SpyWorks can be found at http://www.desaware.com.
For notification when new articles are available, sign up for Desaware's Newsletter.
|Products Purchase Articles Support Company Contact