Desaware Home
Products    Purchase    Publishing    Articles   Support    Company    Contact    
Articles
.NET
COM

 

 

bluebar
Contact Desaware and order today

bluebar
Sign up for Desaware's Newsletter for the latest news and tech tips.

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.

Contents:

You can download the sample code from ftp.desaware.com/SampleCode/Articles/Waitfor.zip. Refer to References section for information on installation.

The problem with waiting

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:

  • You can wait for messages to be placed in a message queue.
  • You can wait for an application or thread to end.
  • You can wait for changes to occur in a directory on disk.
  • You can wait for changes to be made to a part of the registry.
  • You can wait for another application to complete some task.
  • You can wait for a specified amount of time to elapse, or for a specific time.

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.

Waiting with the SpyWorks Background Thread Component

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.

A simple approach to waiting on synchronization objects

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:

  1. Launch an instance of the WaitForTest application, click on the "Start Wait" button, then click on the "Abort Wait" button.
  2. Launch two instances of the WaitForTest application, click on the "Start Wait" button in one, then on the "Signal Event" button on the other.

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 WaitFor2 example: Improving the architecture

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.

  1. The main class and background class have to interact quite a bit. The principles of object oriented design encourage one to explore ways to make the two classes more independent of each other.
  2. It is impossible to call methods or properties of the background class while it is waiting. This is because the apartment threading model requires all method and property calls to that object to be on the same thread - the one that is frozen during the wait operation.

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.

Testing

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.

Conclusion

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.

References

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.

articles
Related Products:
 
Products    Purchase    Articles    Support    Company    Contact
Copyright© 2012 Desaware, Inc. All Rights Reserved.    Privacy Policy