Reduce Long Waits with Threads


Write a generic thread manager that uses multithreading to sidestep time-consuming operations.

by R. Mark Tucker

Reprinted with permission from Visual Basic Programmer's Journal, 2/98, Volume 8, Issue 2, Copyright 1998, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange at www.devx.com

The deluge of new uses for multithreading is a topic as hot as an Arizona summer, but the best, most practical use for it remains unchanged. A multi-threaded app keeps long operations from blocking the rest of your application, allowing your users to continue working as those operations complete in the background. This not only reduces your users' frustration, but also makes them more productive. For example, database queries, file I/O, and printer functions can all take an excessive amount of time, but multiple threads ensure your application doesn't grind to a halt as it waits for them to complete.

One way to implement multithreading in your apps is to write a generic thread manager (see Figure 1). A thread manager acts as a middleman between the client application and one or more objects that you want to run on separate threads. When the manager receives a request from the client to execute a long operation using a specific object, it assumes responsibility for completing the task and frees the client app to respond to more user requests. It is the thread manager's job to create the "managed" object on an individual thread, instruct it to perform its function, and clean up the memory used by the object when it completes its task. You can use the manager presented here in any project where an operation needs to occur on a separate thread. The manager can handle multiple "managed" objects from a single component or multiple components.

Note that implementing multithreading is not a trivial task. One difficulty is determining what VB does for you behind the scenes, and figuring out when VB will actually create an object on a separate thread. This article doesn't explain how multi-threading works, but shows you what you need to do to put it to use in your applications. See Ted Pattison's article, "Master Multithreading-Carefully," in this issue for more information on the basics of multithreading. You can also find valuable resources on multithreading in the sidebar, "Do Your Homework First."

Do Your Homework First
Multithreading in VB requires knowledge in many areas, some of which are new to VB5. These areas include automation, component instancing, interfaces, callbacks, and events. Though you might be tempted to jump right in and start coding, it's important to understand more than just the basics. Ignoring this advice might be likened to opening Pandora's box.

The first place to look is Visual Basic Books Online. Do a search on "threads" and read these articles and their associated links: "Scalability and Multithreading" and "Creating ActiveX Components." Next, search MSDN Library Online (www.microsoft.com/msdn) for the article, "Processes and Threads." There are also a number of valuable articles, including Daniel Appleman's "A Thread to Visual Basic" (www.desaware.com/desaware/thartrq.htm), Ted Pattison's "Create More Scalable Objects with DCOM" [VBPJ December 1997], Ibrahim Malluf's "Create Multithreaded Objects" [VBPJ May 1997], Lee The's "Inside VB" [VBPJ September 1997], and Deborah Kurata's "Program with Events" [VBPJ November 1997]. It's important that you assimilate and understand the concepts presented in these articles.   -R.M.T.


No article about threads would be complete without a few words to the effect of, "You don't have to take up skydiving just because your friend has a plane." Multithreading makes a good deal of sense when you want to let your users continue working as a long operation executes, but there are many cases when multithreading would slow down or cripple your app. For example, if two tasks take about the same amount of time, a single processor can accomplish the tasks faster if you execute them one after the other. Multithreading would take longer in this case because of the extra overhead in managing the separate threads. The user would see a slower, less responsive app. Multithreading also complicates the debugging process.

How threads work
You need to take a careful look at when VB creates an object on a separate thread before you begin building the thread manager. The type of ActiveX component you choose to create, its threading model, an object's Instancing property, and how you instantiate the object all affect whether VB creates an object on a new thread, on an existing (single) thread, or in a separate process. If you don't handle this correctly, you could be creating new processes when you don't intend to.

Each thread belongs to its own apartment, and each apartment gets its own copy of global data and global objects in VB's implementation of apartment-model threading. Objects on the same thread can share variables declared as Public in a standard module. The danger is that you have little control over which objects will share a thread. As a general rule, you should avoid creating your own global variables in your multithreaded applications unless you know how VB will interpret them. Global variables can be useful, however, if you think of them in terms of apartments. For example, consider the App object, which VB creates in each apartment automatically. You can use the App.ThreadID property to obtain the Win32 thread ID that uniquely identifies each thread. The thread manager described in this article uses the thread ID to keep track of the objects it manages.

Note that the Instancing property of an object in an ActiveX component affects where VB instantiates the object. If the object's Instancing property is set to SingleUse (or GlobalSingleUse), VB creates a new process (with a new primary thread) each time you use New or CreateObject in the client to instantiate the object. VB also creates that object in a new process if an object in the server creates another object using CreateObject, and that object has an Instancing property of SingleUse (see Figure 2). This option isn't available for ActiveX DLLs, however. This makes sense because an in-process component is created "in the same process" as its client. You can implement a form of "multithreading" without diving into threads too deeply by setting an object's Instancing property to SingleUse. This approach forces VB to create each object in its own process, which requires extra resources.

The Project Properties dialog determines which thread VB will create an object on when you set an object class's Instancing property to MultiUse or GlobalMultiUse (see Figure 3). Visual Basic provides three options for assigning objects to threads for out-of-process components: thread per object, single thread of execution, and thread pool.

Single-threaded execution is the default when creating an ActiveX EXE component, and you use this for creating most of your components. Multithreaded components require that you choose one of the other options, however. You must specify the number of threads in the pool when you compile a component using the thread-pool option. This number often equals the number of processors on a multiprocessor system, but you might want to specify more than one thread on a single-processor system when you know that much of a thread's time will be in a blocked mode, waiting for another server.

VB creates an object on the next thread in a round-robin fashion when a client creates an object with New or CreateObject. If you use the New operator to create an object in a server, then the object is a dependent object and is created on the same thread. An object created within the server using CreateObject is treated as if the client created it, and VB creates it on the next thread. Note that each thread gets its own copy of global data and objects. You have no way of predicting which objects, aside from dependent objects, will share a thread and the global information.

Life is simpler when you compile using the thread-per-object option. This option allows you to create each object on its own thread, rather than the next thread in the pool. Each object also has its own App object, its own copy of global data, and its own copy of other global objects. The downside of this approach is that you have no control over how many objects (threads) are created. I created the sample project using VB5 Service Pack 2 (SP2). You don't need SP2 to write a thread manager, but there might be slight variations in how the project works if you use a different version.

Write a Thread Manager
Now that you know when VB creates a new thread, let's build a thread manager and a managed-thread object to complete a task asynchronously at the request of the client application. I chose to compile the SimpleThreadManager component as an apartment-threaded ActiveX DLL. If you prefer, you can compile the SimpleThreadManager as an ActiveX EXE. A DLL is faster, but if an error in the client causes the client to crash, any ActiveX DLLs also crash, and some manager tasks might not complete. Another disadvantage of an ActiveX DLL is that there might be times when the client would block the thread that it and the DLL share, and the thread manager would have to wait for the client to finish before the manager could continue.

The thread manager contains an internal collection of object references where each object is on its own thread. Each of these objects implements the IManagedThread interface:

IManagedThread.cls
Public Property Get ThreadID() As Long
End Property
Public Sub SetThreadManager _
   (ThreadManager As ThreadManager)
End Sub
Public Sub ReleaseThreadManager()
End Sub
Public Sub Execute(Async As Boolean, _
   Custom As Variant)
End Sub

The thread manager also handles creating and cleaning up threads. The CreateManagedThread function creates a new instance of a class based on the IManagedThread interface:

Private Function CreateManagedThread _
   (Class As String) As IManagedThread
   Dim objThread As IManagedThread
   Set objThread = CreateObject(Class)
   'tell object who its manager is 
   objThread.SetThreadManager Me
   mcolThreads.Add objThread, "T" & _ 
      Str(objThread.ThreadID)
   Set CreateManagedThread = objThread
End Function

The CreateObject function allows the client application to specify the class (appname.objecttype) of the object to create. In this example, New and CreateObject both create the object on its own thread because the managed thread class is defined in an out-of-process component. The thread manager can handle many different objects by using CreateObject and making sure that every managed thread object implements the IManagedThread interface. Next, the created object receives a reference to its thread manager.

You want the thread to notify the thread manager when the managed thread finishes its task. You can't use events in this situation because the object reference is stored in a collection. Instead, use the thread ID as part of the key to store the object reference in the thread manager's collection. This is the only reference to the thread object while it is active. The thread-pool option wouldn't work in this situation because different objects on the same thread would have the same thread ID, and you would be trying to insert duplicate keys into the collection.

The Execute method is the only method you call from the client application. The first parameter allows the client to specify which managed object it wants the thread manager to create. The managed object encapsulates some specific job to be done. For example, assume you have an object called DoSomething contained in the ThingsToDo component. The client application calls the thread manager's Execute method, passing ThingsTo-Do.DoSomething in the class parameter, when it wants to perform an operation on a separate thread. The optional Custom parameter allows the client to pass parameters to the object and the object to return values to the client. This method calls CreateManagedThread to create an instance of that object. Next, the thread manager calls the managed object's Execute method, passing it the Custom data. Setting the Async parameter to True causes the managed object to execute asynchronously, and the ThreadManager to return control to the client application immediately, without blocking its thread:

Public Sub Execute(Class As String, _
   Optional Custom As Variant)
   Dim objThread As IManagedThread
   Set objThread = CreateManagedThread (Class)
      'asynchronous call to do task
      objThread.Execute True, Custom
   RaiseEvent Started(Custom)
      Set objThread = Nothing
End Sub

The managed thread employs a callback to call the CleanupThread method when it finishes its task or encounters an error:

Public Sub CleanupThread(ThreadID As Long, _
   Success As Boolean, Custom As Variant)
   Dim objThread As IManagedThread
   Dim strIndex As String
   strIndex = "T" & CStr(ThreadID)
   'break circular reference
   Set objThread = mcolThreads.Item(strIndex)  
   objThread.ReleaseThreadManager
   'release reference to free thread
   mcolThreads.Remove strIndex
    RaiseEvent Completed(Success, Custom)
End Sub

The thread manager then calls the ReleaseThreadManager method of the managed-thread object to break its reference to the manager. The thread manager removes the object reference to the managed thread from its collection, and the thread is freed because this is the only remaining reference.

Create a Managed-Thread Component
Next, you need to create a managed-thread object so the thread manager has something to manage. A managed-thread object can perform any task you want to execute without blocking your client application. To keep this example simple, the managed-thread object calls VB's Dir function using a passed string, then calls the Sleep API function to pause the object's thread for a specified time. The pause allows you to see whether the client application is blocked by the managed object's thread. You can also use PView to see the number of threads within the server process. Now, call the managed-thread object FileSearch. Set the Instancing property of the FileSearch object to MultiUse, and compile MultithreadComp as an ActiveX EXE using the thread-per-object threading model.

The managed-thread component also contains a standard module to handle calls to the timer functions in the Windows API (see Listing 1). This module declares the timer functions and provides a callback function, TimerProc, that allows the object to execute asynchronously. The module contains the only global variable in the server, which you set in the object's Class_Initialize event. If you prefer, you can compile the timer code into an ActiveX DLL that the managed-thread class uses, eliminating the global variable entirely. You can find an example of this in the Xtimers project that ships with VB.

Listing 1

ThreadTimer.bas

Private Declare Function SetTimer Lib _
   "user32" (ByVal hwnd As Long, _
   ByVal nIDEvent As Long, ByVal uElapse As Long, _
   ByVal lpTimerFunc As Long) As Long
Private Declare Function KillTimer Lib _
   "user32" (ByVal hwnd As Long, _
   ByVal nIDEvent As Long) As Long

Private mlngTimerID As Long
Private mvarCustom As Variant
Public gobjThread As IManagedThread

Private Sub TimerProc(ByVal hwnd As Long, _
   ByVal msg As Long, ByVal idEvent As Long, _
   ByVal curTime As Long)

      StopTimer

      gobjThread.Execute False, mvarCustom
   
   mvarCustom = Empty
      Set gobjThread = Nothing
End Sub

Public Sub StartTimer(Custom As Variant)
   mvarCustom = Custom
   mlngTimerID = SetTimer(0, 0, 100, _
      AddressOf TimerProc)
End Sub

Public Sub StopTimer()
   If mlngTimerID > 0 Then
      KillTimer 0, mlngTimerID
         mlngTimerID = 0
   End If
End Sub
Listing 1 Implement an Asynchronous Method Call. The THREADTIMER.BAS module contains the code to create a formless timer. It is contained in the same component as the managed-thread objects and is used when the ThreadManager calls a managed object's Execute method. The object then starts the timer and returns control back to the ThreadManager, which returns control back to the client. Several managed objects within the same process can share this module because of the way VB implements apartment threading.
The FileSearch object implements the IManagedThread interface and creates a private object reference to the thread manager (see Listing 2). The private ExecuteTask method contains the actual work that the FileSearch object will do. The managed thread tells the thread manager to release it by calling the CleanupThread method when the FileSearch object finishes its task. The remaining property and methods are part of the IManagedThread interface. The Execute method calls the managed thread's private ExecuteTask method or starts the timer, depending on the value of the Async parameter. The SetThreadManager and ReleaseThreadManager methods simply manipulate the private object reference to the thread manager.

Listing 2

FileSearch.cls

Private Declare Sub Sleep Lib "kernel32" _
   (ByVal dwMilliseconds As Long)

Implements IManagedThread
Private mobjThreadManager As ThreadManager

Private Sub Class_Initialize()
   Set gobjThread = Me
End Sub

Private Sub Class_Terminate()
   StopTimer

   If Not mobjThreadManager Is Nothing _
      Then Set mobjThreadManager = Nothing
   If Not gobjThread Is Nothing Then _
      Set gobjThread = Nothing
End Sub

Private Sub ExecuteTask(Custom As Variant)
   Dim strSearch As String
   Dim lngPause As Long
   Dim colValues As Collection

   '*************************************
      'Insert Task to be accomplished here!
      'get passed values
      strSearch = Custom(0)
      lngPause = Custom(1)

      'make room for return values
      ReDim Preserve Custom(2)

      Set colValues = FindFiles(strSearch)
      If colValues.Count = 0 Then
         Custom(2) = Empty
      Else
         Set Custom(2) = colValues
      End If

      If lngPause > 0 Then Sleep lngPause
      '*************************************

      'tell Thread Manager to release its 
   'reference to the object
   mobjThreadManager.CleanupThread _
      App.ThreadID, True, Custom  
End Sub

Private Sub IManagedThread_Execute(Async As _
   Boolean, Custom As Variant)
   If Async = True Then
         'Execute task asynchronously
         StartTimer Custom
      Else
         'Execute task synchronously
         ExecuteTask Custom
      End If
End Sub

Private Sub 
   IManagedThread_ReleaseThreadManager()
   Set mobjThreadManager = Nothing
End Sub

Private Sub IManagedThread_SetThreadManager _
   (ThreadManager As ThreadManager)

   Set mobjThreadManager = ThreadManager
End Sub

Private Property Get _
   IManagedThread_ThreadID() As Long

   IManagedThread_ThreadID = App.ThreadID
End Property

Private Function FindFiles(Search As String) As _
   Collection
   Dim strValue As String
      Dim colValues As New Collection

      strValue = Dir(Search, vbNormal)
      Do While strValue <> ""
         colValues.Add strValue
         strValue = Dir
      Loop

      Set FindFiles = colValues
      Set colValues = Nothing
End Function
Listing 2 Write a Search Thread. The FileSearch object implements the IManagedThread interface and creates a private object reference to the thread manager. The FileSearch object is a simple example of an operation on one thread executing without blocking the client thread. The code, except for the task you want to perform (found in the ExecuteTask method), is largely cut-and-paste.
Test the Components
You must compile the SimpleThreadManager DLL and the MultithreadComp EXE to test the components, starting with the SimpleThreadManager DLL. It is important that you compile them in the correct order because both the client and server contain a reference to the thread-manager component. It's possible to do some limited end-to-end testing using the VB debugger, but you can't do a full test without compiling because the VB IDE is single-threaded.

Start by running the SimpleThread-Manager project, then open the Multi-threadComp project in another instance of VB and make sure its reference is set to the thread-manager project and not to the compiled DLL. Next, run the MultithreadComp project. Load the client application in a third instance of VB, and set its reference to the thread-manager project. Run the client. Set breakpoints wherever you want, and click on the Start button on the client-tester app. You should be able to trace the execution throughout all three projects at this point. Note that the thread ID of all objects created by the ThreadManager is the same, so you can only trace the creation of the first object. The client tells the ThreadManager to execute some task by calling its Execute method and passing it the appname.objecttype of the class that handles that task. The call for the FileSearch operation looks like this:

mobjThreadManager.Execute _
   "MultithreadComp.FileSearch"

This call returns immediately, and the client is free to perform other tasks. If you use WithEvents to create the instance of the ThreadManager, the client can receive the ThreadManager's Started, Completed, and Error events. The Execute method also allows for an optional variant argument to pass custom data to and receive values back from the threaded object. In this example, you pass FileSearch a search string and a pause value. FileSearch then returns a collection of strings with names of all files found in the search directory.

You can now see VB multithreading in action. Each time you click on the Start button, the thread manager creates a new thread. You can view these threads with the PView program (see Figure 4), which you find in the PView directory under the Tools menu on your VB CD. When the thread count drops back to zero, it means that no object in MultithreadComp is referenced, and the component is unloaded.

That's about it. Programming threads in VB requires a clear understanding of many topics, but there is a big payoff for putting in the time required. The thread manager I've shown you how to create is simply a starting point. You can augment or change the functionality of the thread manager in a number of ways. For example, you might add a MaxThread property to control the number of threads the manager can create. The manager might store requests for threads and create them when another thread is complete. This would allow you to control the number of threads created at run time when you compile with the thread-per-object option.

You also might implement an IThreadManager interface in the thread manager. This would allow you to create different thread managers, while ensuring that your ManagedThread objects can be used with any of them. If you add such an interface, you should probably define the IThreadManager and IManagedThread interfaces in a separate typelib. The client, thread manager, and server objects would then reference that typelib. I'm sure you have your own ideas for using and extending this thread manager as well. Regardless of how you choose to augment or change the thread manager's functionality, your users no longer have to wait for a single, long operation to finish before they can continue working.


R. Mark Tucker is a consultant with PSC Inc., a subsidiary of Data Processing Resources Corp. (DPRC). He specializes in client/server development and component design. Mark graduated from Brigham Young University with a B.S. degree in management information systems and currently lives and works in the Phoenix area. Reach him at mtucker@iname.com.