Creating a Multithreaded Test Application

In order to test and debug in-process components (.dll and .ocx files), you will need a multithreaded client application. The steps to create a simple multithreaded application are as follows:

  1. Open a new ActiveX EXE project, and name the default class module MainApp. Set the Instancing property for MainApp to PublicNotCreatable. A MainApp object will occupy the application's first thread, and display the main user interface.

  2. On the General tab of the Project Properties dialog box, select Sub Main in the Startup Object box, select Thread per Object in the Threading Model box, and enter a unique value for the Project name. (Project Name determines the type library name; problems may arise if two applications have the same type library name.) "ThreadDemo" is the project name used in the example below. On the Component tab, select Standalone in the Start Mode box.

  3. Add a form, name it frmProcess, and set the Visible and ControlBox properties to False. This form functions as a hidden window, which Sub Main will use to identify the main thread for the process. No code is needed for this form.

  4. Add a standard module to the project. In this module, place the declarations, the Sub Main procedure, and the EnumThreadWndMain procedure shown below. As explained in the accompanying text and code comments, Sub Main will execute when your application starts, and every time you create a new thread. The Sub Main sample code demonstrates how to identify the first thread, so that you know when to create MainApp.

  5. Add a form and name it frmMTMain. This form provides the main user interface for the test application. Add to it the single declaration and the Form_Unload event immediately above the heading "Multiple Instances of the Test Application."

  6. In the Class_Initialize event procedure for MainApp, add code to show frmMTMain. Code to do this is provided below.

  7. To create additional test threads, you must have at least one class in your project whose Instancing property is set to MultiUse. Add a class module and form and insert the code provided under the heading "Creating New Threads." Because you selected Thread per Object for this project, every public object that is externally created will start on a new thread. This means that you can create a new thread by using the CreateObject function to create an instance of your MultiUse class from its programmatic ID (ProgID), as discussed in the accompanying text.

  8. Add code to frmMTMain to create new threads by creating instances of the MultiUse classes you defined. In the example below see the code under the heading "Creating New Threads."

  9. The development environment does not support multithreading. If you press F5 to run the project, all objects will be created on the same thread. In order to test multithreaded behavior, you must compile the ActiveX EXE project, and run the resulting executable.

Important   To ensure that each new MultiUse object starts a new thread, you must use Thread per Object rather than Thread Pool.

Determining the Main Thread During Sub Main

Sub Main executes for every new thread. The reason for this is that Visual Basic maintains a separate copy of your global data for each thread (each apartment). In order to initialize global data for the thread, Sub Main must execute. This means that if your Sub Main loads a hidden form, or displays your application's main user interface, new copies of those forms will be loaded for every new thread you create.

The following code determines whether or not Sub Main is executing in the first thread, so that you can load the hidden form only once and display the test application's main user interface only once.

' Root value for hidden window caption
Public Const PROC_CAPTION = "ApartmentDemoProcessWindow"

Public Const ERR_InternalStartup = &H600
Public Const ERR_NoAutomation = &H601

Public Const ENUM_STOP = 0
Public Const ENUM_CONTINUE = 1

Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
   (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Declare Function GetWindowThreadProcessId Lib "user32"_
   (ByVal hwnd As Long, lpdwProcessId As Long) As Long
Declare Function EnumThreadWindows Lib "user32" _
   (ByVal dwThreadId As Long, ByVal lpfn As Long, ByVal lParam As Long) _
   As Long

' Window handle retrieved by EnumThreadWindows.
Private mhwndVB As Long
' Hidden form used to identify main thread.
Private mfrmProcess As New frmProcess
' Process ID.
Private mlngProcessID As Long

Sub Main()
   Dim ma As MainApp

   ' Borrow a window handle to use to obtain the process
   '   ID (see EnumThreadWndMain call-back, below).
   Call EnumThreadWindows(App.ThreadID, AddressOf EnumThreadWndMain, 0&)
   If mhwndVB = 0 Then
      Err.Raise ERR_InternalStartup + vbObjectError, , _
      "Internal error starting thread"
   Else
      Call GetWindowThreadProcessId(mhwndVB, mlngProcessID)
      ' The process ID makes the hidden window caption unique.
      If 0 = FindWindow(vbNullString, PROC_CAPTION & CStr(mlngProcessID)) Then
            ' The window wasn't found, so this is the first thread.
            If App.StartMode = vbSModeStandalone Then
               ' Create hidden form with unique caption. 
               mfrmProcess.Caption = PROC_CAPTION & CStr(mlngProcessID)
               ' The Initialize event of MainApp (Instancing =
               '   PublicNotCreatable) shows the main user interface.
               Set ma = New MainApp
               ' (Application shutdown is simpler if there is no
               '   global reference to MainApp; instead, MainApp
               '   should pass Me to the main user form, so that
               '   the form keeps MainApp from terminating.)
            Else
               Err.Raise ERR_NoAutomation + vbObjectError, , _
                  "Application can't be started with Automation"
            End If
         End If
   End If
End Sub

' Call-back function used by EnumThreadWindows.
Public Function EnumThreadWndMain(ByVal hwnd As Long, ByVal _
   lParam As Long) As Long
   ' Save the window handle.
   mhwndVB = hwnd
   ' The first window is the only one required.
   ' Stop the iteration as soon as a window has been found.
   EnumThreadWndMain = ENUM_STOP
End Function

' MainApp calls this Sub in its Terminate event;
'   otherwise the hidden form will keep the 
'   application from closing.
Public Sub FreeProcessWindow()
   Unload mfrmProcess
   Set mfrmProcess = Nothing
End Sub

Note   This technique for identifying the first thread may not work in future versions of Visual Basic.

Notice that Sub Main takes no action for any thread after the first. When you add code that creates MultiUse objects (in order to start subsequent threads) be sure to include code to initialize those objects.

EnumThreadWindows is used with a call-back, EnumThreadWndMain, to locate one of the hidden windows Visual Basic creates for its internal use. The window handle of this hidden window is passed to GetWindowThreadProcessId, which returns the process ID. The process ID is then used to create a unique caption for the hidden window (frmProcess) that Sub Main loads. Subsequent threads detect this window, and thus can tell that they don't need to create the MainApp object. These gyrations are necessary because Visual Basic does not provide a way to identify the application's main thread.

The MainApp class, in its Initialize event, displays the test application's main form. MainApp should pass its Me reference to the main form, so that the form keeps MainApp from terminating. From the main user interface you can create all subsequent threads. Setting the Instancing property for MainApp to PublicNotCreatable helps you avoid displaying two main user interface forms.

A simple example of a MainApp class and its associated form (steps 5 and 6, above) might look like this:

' Code for a MainApp class.
Private mfrmMTMain As New frmMTMain

Private Sub Class_Initialize()
   Set mfrmMTMain.MainApp = Me
   mfrmMTMain.Caption = mfrmMTMain.Caption & " (" & App.ThreadID & ")"
   mfrmMTMain.Show
End Sub

Friend Sub Closing()
   Set mfrmMTMain = Nothing
End Sub

Private Sub Class_Terminate()
   ' Clean up the hidden window.
   Call FreeProcessWindow
End Sub

' Code for the form frmMTMain.
Public MainApp As MainApp

Private Sub Form_Unload(Cancel As Integer)
   Call MainApp.Closing
   Set MainApp = Nothing
End Sub

Multiple Instances of the Test Application

Including the process ID in the hidden window caption allows multiple instances of the test application to run without interfering with each other.

When you call CreateObject, the instance of the public class you create will be on a thread in the current application instance. This is because CreateObject always attempts to create an object in the current application before looking for other running Exe components that might supply the object.

Useful Properties for the Apartment

You may find it useful to expose the process ID as a read-only property of the module that contains Sub Main:

'This code not required for the test application
Public Property Get ProcessID() As Long
   ProcessID = mlngProcessID
End Property

This allows any object on the thread to get the process ID by calling the unqualified ProcessID property. You may also find it useful to expose a Boolean IsMainThread property in this fashion.

Creating New Threads

The Thread per Object option causes every public object that is externally created - that is, created using the CreateObject function - to start on a new thread. To create a new thread, simply use the programmatic ID (ProgID) of one of your MultiUse classes:

'This code not included in the test application
Dim tw As ThreadedWindow
Set tw = CreateObject("ThreadDemo.ThreadedWindow")

The variable tw now contains a reference to an object on a new thread. All calls to the properties and methods of this object that are made using tw will be subject to the extra overhead of cross-thread marshaling.

Note   An object created with the New operator is not created on a new thread. It resides on the same thread where the New operator was executed. See "Designing Multithreaded Out-of-Process Components" and "How Object Creation Works in Visual Basic Components,."

To ensure that MainApp doesn't terminate until all of the other threads are finished, you can give each public class a MainApp property. When an object creates a MultiUse object on a new thread, it can pass the new object a reference to the MainApp object as part of the initialization process. (You can also pass MainApp a reference to the new object, so that MainApp has a collection of references to all objects that control threads; however, remember that this will create circular references. See "Dealing with Circular References.")

If you want a class that controls a thread to show a form, you should provide it with an Initialize method (not to be confused with the Initialize event) or a Show method that displays the form. Don't show the form in the Class_Initialize event procedure, as this could cause timing errors when you create instances of the class. In a very simple case, the code for a MultiUse ThreadedWindow class and its form, frmThreadedWindow, might look like this:

'Code for a MultiUse ThreadedWindow class.
Private mMainApp As MainApp
Private mfrm As New frmThreadedWindow

Public Sub Initialize(ByVal ma As MainApp)
   Set mMainApp = ma
   Set mfrm.ThreadedWindow = Me
   mfrm.Caption = mfrm.Caption & " (" & App.ThreadID & ")"
   mfrm.Show
End Sub

Friend Sub Closing()
   Set mfrm = Nothing
End Sub

'Code for the form frmThreadedWindow.
Public ThreadedWindow As ThreadedWindow

Private Sub Form_Unload(Cancel As Integer)
   Call ThreadedWindow.Closing
   Set ThreadedWindow = Nothing
End Sub

The following code snippet shows how you might initialize the ThreadedWindow object:

'Code for the test application's main form (frmMTMain).
Private Sub mnuFileNewTW_Click()
   Dim tw As ThreadedWindow
   Set tw = CreateObject("ThreadDemo.ThreadedWindow")
   ' Tell the new object to show its form, and
   '   pass it a reference to the main
   '   application object.
   Call tw.Initialize(Me.MainApp)
End Sub

If you have a number of classes that can control threads, you can make your code more generic by defining an IApartment interface to contain the Initialize method. When you implement IApartment in each class, you can provide the appropriate Initialize method code for that class. Your thread creation code might look like this:

'This code not required for the test application
Private Sub mnuFileNewObject_Click(Index As Integer)
   Dim iapt As IApartment
   Select Case Index
     Case otThreadedWindow
      Set iapt = CreateObject("ThreadDemo.ThreadedWindow")
     ' (other cases...)
   End Select
   ' Common code to initialize objects.
   Call iapt.Initialize(MainApp)
End Sub

Note   You can make an IXxxxApartment interface that's known only to the multithreaded application by defining the interface in a separate type library. In the ActiveX Exe project, set a reference to the type library.

Keeping References to Threaded Objects

To ensure proper shutdown of a multithreaded application, you must keep careful track of all references to the MultiUse objects you use to create and control threads.

Define your object lifetime goals clearly. For example, consider the case of a MultiUse object that shows a form. The easiest way to manage object lifetime is to have the object pass the form a Me reference; the form then keeps the object alive. When the user closes the form, the form's Unload event must set all references to the MultiUse object to Nothing, so that the object can terminate and in turn clean up its reference to the form. (You may find it useful to give the MultiUse object a Friend method that cleans up the reference to the form, and all other internal object references; the form's Unload event can call this method.)

If the object that controls a thread creates additional objects on the thread, using the New operator, make sure you clean up references to those objects. The thread cannot close until all references to objects that were created on the thread have been released. Open threads consume system resources.

Friend Methods Cannot Be Used Cross-Thread

Because Friend properties and methods are not part of the public interface of a class, you cannot call them from another thread. Cross-thread calls between objects are limited to properties and methods that are declared Public.

Reentrancy

If a method of an object yields by calling DoEvents, showing a modal form, or making a secondary call to an object on another thread, then the code in the method can be entered by a second caller before the first call completes. If such a method uses or changes property values or module-level variables, this could result in an invalid internal state for the object. To protect against reentrancy, you can:

Asynchronous Tasks

Visual Basic doesn't provide a way to fork execution - that is, to have one thread initiate a method call on a new thread and immediately resume processing on the original thread. You can simulate this behavior in your test application by having the original method call turn on a timer and then return immediately. When the timer event occurs, you can turn the timer off and perform the asynchronous processing. This technique is discussed in "Asynchronous Call-Backs and Events", and is demonstrated (see "Creating an ActiveX Exe Component") and in the Coffee sample application.

Using the Multithreaded Test Application

You must compile the multithreaded test application in order to test your apartment-threaded component, because the Visual Basic development environment does not currently support multiple threads of execution. If you have Visual Studio, you may find it useful to compile the test application with debugging information for native code debugging, so that you can use the Visual Studio debugger.