Visual Basic Banner Exchange

June 1998

Return to Article Index

Using System Event objects to implement multi-threaded asynchronous calling in Visual Basic 5.0
By Andrew Gayter


As applications become more sophisticated, due to complexity and market pressures, many developers are turning to multi-threaded techniques to increase responsiveness and take advantage of multi-processor architectures. In many cases a multi-threaded application is appropriate, providing asynchronous task execution and superior user interface interaction. In other cases multi-threaded applications are essential due to performance criteria, such as server and database applications.

Traditionally, multi-threaded applications in Visual Basic are cumbersome and use inefficient polling mechanisms based on Windows timers. Often to provide shared data synchronization a mixed apartment model is necessary, which extends the inefficiency due to cross-apartment/process marshaling. Only one real mechanism has been proposed for implementing asynchronous calling in VB. What I am going to introduce is a new way to implement asynchronous calling using System Event objects (Accessible through the WIN32 API). The implementation only requires that the project be an out an process free threaded executable written in Visual Basic 5.0, SP3. The implementation works on both Windows 95 (DCOM installed) and Windows NT4.0, SP3. I am not saying that the implementations won't work on workstations without the latest service packs, but much work has been done to improve the Visual Basic 5.0 COM implementation in the more recent packs.

In tests performing 1000 asynchronous calls the method described out-performed the timer method by 10 - 15%.

Background

Processes and Threads

With the introduction of Windows NT and Windows 95 came a new memory model and a new multitasking environment. With these new operating systems came the ability to run applications in separate memory spaces, with no sharing of DLLs as was the case under Windows 3.x. Unstable applications are now less likely to bring down the whole machine, as each application has it’s own message loop and memory space and so are distinct from each other.

A process can best be described as being a task. Tasks can be viewed under both operating systems by doing the old ‘three figure shuffle’ CTRL+ALT+DEL. Under WIN95 this brings up the current tasks and under WINNT it gives you the option to run Task Manager.

For example. Running Word and Excel would result in two processes being created. They are totally separate from each other and if one crashes (I bet it will be Word) it will not affect the execution of the other.

A process has at least one main thread of execution, commonly called the controlling thread. However, a process may also have other threads of execution. Each thread of execution within a process is scheduled CPU time by the operating system. Each time a thread is scheduled the operating system performs a context swap, which is a removal and replacement of the stack and registers. This enables many threads of execution to appear as though they are running simultaneously. Infact only one thread at a time receives execution cycles. The time that is scheduled to a thread is dependent upon the process priority and the threads own priority status.

In Visual Basic any executable runs in its own process space. A DLL exists in the process space of an executable. In COM the terms out-of process means an executable and in-process means a DLL. The Apartment Model has been developed to handle many of the synchronization issues concerned with cross process marshaling. I’m not going to go any further, as the Apartment Model and process models are really topics in their own right.

System objects

Windows supports a great number of different objects. The most common that most of us all know about are the Graphical User Interface (GDI) and file objects.

Windows also supports a number of system objects which are process and thread orientated. Examples of these types of objects are event, mutex and semaphore. Many of you may have heard or come across these object types if you have ever been studied many of the popular operating systems such as Unix or VMS.

System objects are created using the objects create functions. Examples include CreateEvent and CreateMutex. Using these create functions it is possible to specify attributes such as security attributes (important if your developing of Windows NT) and child inheritance.

Event Objects

An event object resides as part of the operating system and have nothing to do with Visual Basic events. It has two states signaled and non-signaled. In the signaled state the event is said to have been ‘raised’.

Public Declare Function CreateEvent& Lib "kernel32" Alias "CreateEventA" (ByVal lpEventAttributes As Long, ByVal bManualReset As Long, ByVal bInitialState As Long, ByVal lpname As String)

dim eventHandle as long

eventHandle = CreateEvent( NULL, TRUE, FALSE, vbNullString )

The function above demonstrates how to create an event object. It has no name, is manual rest and is in a non signaled state. An event object can be created to be either manual reset or auto reset. If auto reset the system becomes responsible for changing the state. In manual reset the two functions below can be used to alter the signaled state. If we were to give the event a name we could access the same event from anywhere within the same project by calling CreateEvent and specifying the same name. This would result in a handle to the same event object being returned.

Public Declare Function ResetEvent& Lib "kernel32" (ByVal hEvent As Long)

Public Declare Function SetEvent& Lib "kernel32" (ByVal hEvent As Long)

call SetEvent( eventHanldle )

The event object has been signaled or 'raised'.

call ResetEvent( eventHandle )

The event object is now in the non-signaled state.

The event object resides as an 'object' in the system. The system is responsible for monitoring the event and the suspension and resuming of threads that are waiting upon it. To place a thread into a wait state on a particular event object we use the WIN32API call WaitOnSingleObject.

Public Declare Function WaitForSingleObject& Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long)

The function is extremely easy to use and it does what it says - it suspends the current thread until the specified event object is in a signaled state.

dim retVal as long

retVal = WaitForSingleObject( eventHandle, WAIT_INFINITE )

We don't need to specify an infinite timeout value. The timeout value can be anything from 0 ms to INFINITE. Specifying a timeout of 0 makes the function return almost immediately.

On returning the retVal variable will either indicate success or failure. Failure can be due to many reasons, but usually where a timeout of anything other than INFINITE is specified, an error code indicating a timeout will be returned i.e The event did not become signaled within the timeout = period.

If the retVal = WAIT_OBJECT_0 (a constant defined as 0) then the function completed and the event object is in a signaled state. From here we can do the work that we have been 'waiting to do'.

if retVal = WAIT_OBJECT_0 then

DO WORK HERE

endif

However, what I have omitted, till now, is the structure of the application that you need to create. The type of application needed is an ActiveX executable. Make sure that the project settings are set to Thread Per Object and that Unattended Execution is checked. These settings set which threading model is to be used.

We firstly need to create a class that will be responsible for making callbacks or execute a given job. By wrapping this functionality into a class gives us much needed functionality i.e an object created on its own thread.

We can contain our controlling code at module level or form level if we wish, as the main thread of the application is responsible for its execution.

sub main()

dim worker as clsWorker

end sub

The class clsWorker is quite simple and contains several properties and methods.

public property Idle as long

public property DoWork as long

public property End as long

The three properties above indicate that the class needs three event objects. An idle event to indicate when the object is idle. A DoWork event which indicates to the object to do some work and finally the end event which I shall explain later.

We create the necessary event objects in the constructor of the class. class_initialize.

Idle = CreateEvent( NULL, TRUE, TRUE, vbNullString )

DoWork = CreateEvent( NULL, TRUE, FALSE, vbNullString )

End = CreateEvent( NULL, TRUE, FALSE, vbNullString )

Notice the signaled states of the events. The idle event is raised - indicating that the object is idle. The methods that we need to implement consist of two public methods Create and Process.

Create is called by the controlling thread to place the object into a suspended state and return execution to the controlling thread. The Create function actually cheats a little bit by creating a windows timer and setting it's timeout to 0. A timer is created at module level using the WIN32API. Module level is necessary, as we cannot pass the 'addressof' a member function to an API function.

Public Declare Function SetTimer& Lib "user32" ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long)

Public Declare Function KillTimer& Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long)

private classHandle as object

private timerHandle as long

public sub SuspendThread( threadObject )

set classHandle threadObject

timerHandle = SetTimer( 0, 0, 0, AddressOf TimerProc)

end sub

sub TimerProc(ByVal hwnd&, ByVal msg&, ByVal id&, ByVal currentTime&)

call KillTimer( timerHandle )

classHandle.Process

end sub

When the timer expires it call the clsWorkers Process routine. The only drawback with using a timer to place the object into its wait routine is that the TimerProc will remain on the stack until the process routine exits.

public sub Create()

SuspendThread me

end sub

I have not finished the implementation as I though it necessary to point out that this function is where all the performance can be gained. As I have already stated the normal use of timers to achieve asynchronous callbacks is a time consuming operation. The process function contains an infinite loop - the time to execution this loop (plus a little system time) is the granularity of your object.

The completed Process subroutine.

public sub Process()

while (true)

if WaitForSingleObject( End, 0 ) WAIT_OBJECT_0 then exit sub

call SetEvent( Idle )

if WaitForSingleObject( DoWork, INFINITE ) = WAIT_OBJECT_O then

if WaitForSingleObject( End, 0 ) WAIT_OBJECT_0 then exit sub

DO WORK

call ResetEvent( DoWork )

if WaitForSingleObject( End, 0 ) WAIT_OBJECT_0 then exit sub

wend

end sub

The first and last WaitForSingleObject calls are testing to see if the infinite loop should be exited. This is important as an object cannot be destroyed until it has resumed execution.

The second Wait statement is actually waiting for the DoWork event to be signaled. When it is then the code indicated by 'DO WORK' will be executed and the cycle restarted. Notice that the idle and DoWork events are reset by the object code. This is because it is only this object that knows how long it is going to take and so is responsible for synchronizing the events.

The controlling routine must be on a different thread. In this example the controlling routine is running on the main application thread. The complete controlling routine may look something like this.

sub main()

dim worker as clsWorker

dim doWorkEventHandle as long

dim idleEventHanlde as long

dim endEventHanlde as long

set worker = createobject("project.clsWorker")

doWorkEventHandle worker.DoWork

idleEventHandle worker.Idle

endEventHandle worker.End

worker.Create

while somethingToDo

if WaitForSingleObject( IdleEventHandle, INFINITE ) = WAIT_OBJECT_0 then

call SetEvent(doWorkEventHandle)

endif

wend

sub end

Notice how we use CreateObject instead of new. CreateObject will always create us an object on a new thread, where as new will only create an object on the current thread.

It is essential that we get the event handles before calling the worker objects create subroutine. If we were to try and obtain the handle after the Create call the application would hang. This is because to get the event handles the worker must execute code - which it can't do if it is in a suspended state i.e waiting on the DoWork event.

Finally, we need to be able to delete the worker object. This is a two phase operation and requires two SetEvent calls. The first is to the workers end event - indicating that the worker should exit. The second is to the workers DoWork event - to release it from it's wait state. If you look back to the clsWorkers Process routine you will see a check is made on the end event directly after the call to wait on the DoWork event. This is to prevent execution on the work code.

call SetEvent( endEventHandle )

call SetEvent( doWorkEventHandle )

set worker = nothing

An interesting point here is that if you do use timers to implement a multi-threaded mechanism, you must be aware that timer messages can get 'backed up' in the message queue. This can result in problems, as execution of the process routine will continue until all messages have been received, resulting in the possibility of automation errors. Another interesting point is that it is possible to 'cut short' the amount of time that a thread executes. Each time a thread is allowed to execute it is given a time slice of CPU time. The thread can voluntarily give up the time slice by calling Sleep with a timeout value of 0.

Conclusion

It can been seen that the implementation of the event object asynchronous calling method is far more complex than the timer implementation. What I have proposed here is an alternative approach to a common problem. I have also introduced the concept of system objects and more specifically event objects.

The increase in performance is also of quite considerable benefit. There is no overhead of starting a timer each time a call is made. In high powered intensive applications an increase in performance of 15% can make all the difference between an application succeeding or failing.

The event object handles exposed can be used to test if the object has completed its task and by simply raising the objects end event the object is guaranteed to be accessible - unlike timers where the threads message queue could get full of timer messages, resulting in continued execution even after no more Process calls are made. Trying to call functions on an object that is currently executing can result in OLE automation errors. These can be difficult to find - as the Visual Basic development environment is not multi-threaded. Often the only means of debugging a multi-threaded application is to write to the event log or to use the VC++ development environment.

Where applications have been written that require a polling mechanism, to look for data or a resource, the timer implementation is very inefficient. If a timeout expires and a condition is not satisfied then the poll and the condition checking has wasted CPU cycles.

The event mechanism described here could quite easily be extended to check several system objects during the wait infinite call. By simply using the WaitForMultipleObjects a system object could be used to indicate that a resource is available and consequently wake up the worker thread. Far more elegant don’t you think ?


About the Author

Copyright 1995-1998 VB Online. All rights reserved.