As you have seen, it's fairly simple to create a multithreaded out-of-process server using Visual Basic. All you have to do is set the appropriate threading mode before you build an ActiveX EXE project. You can also create a form-based application that uses multiple threads (although this is far more difficult). Thus you can create a secondary thread in a typical Windows application, which allows you to run a task in the background without freezing the user interface. This can make a huge difference in a desktop application in which running certain tasks on the primary thread would lock down the user interface for an unacceptable period of time.
The technique for running a secondary thread requires using a back door to create a second apartment. Although this requires a little trickery, the results are both safe and predictable. If you create a form-based application with an ActiveX EXE project instead of a Standard EXE project, Visual Basic includes built-in support for creating additional apartments. The trick is to make an internal activation request seem as if it's coming from a remote client. As you'll recall from earlier in this chapter, a thread-per-object ActiveX EXE creates a new apartment when an external client makes an activation request. You can trick the ActiveX EXE into thinking that you're a remote client by using Visual Basic's CreateObject function instead of the New operator.
When you create an object in an ActiveX EXE project using the New operator, the Visual Basic run time will attempt to activate the object directly from the server. If the ActiveX EXE contains a local definition of the CLSID, the object is activated in the same apartment of the client that named New. If there's no local implementation for the CLSID, the Visual Basic run time passes the activation request to the SCM. Unlike a call to New, however, a call to CreateObject always results in a call to the SCM. If you pass the ProgID of a local class to the CreateObject function, your server calls the SCM and the SCM responds by calling back into your server's process in the same manner as with any other external activation request. The ActiveX EXE sees an external activation request and creates the object in a new STA.
What happens when you call CreateObject from within your thread-per-object ActiveX EXE project? First a new STA is created in your process and the new object is loaded inside it. Next CreateObject returns an interface reference to the client, which is bound to the object through a proxy/stub layer. Now you have an object in a separate apartment running on a different thread, as shown in Figure 7-10. OK, you've done the easy part. Doing anything meaningful with this new apartment and thread requires significant work.
Figure 7-10. You can create secondary threads in a Visual Basic application with a user interface. You can create a new thread in a thread-per-object ActiveX EXE application by creating an object with the CreateObject function instead of the New operator.
The first obstacle that you must face is that you can't run or debug your multithreaded client application in the Visual Basic IDE. The behavior of creating additional STAs can be exhibited only in a compiled EXE. This makes it far more difficult to debug your application. But then again, what could be more reliable in debugging than your old friend the MsgBox statement?
When you create an ActiveX EXE instead of a Standard EXE, you must use Sub Main instead of a startup form. This means that you must use Sub Main to load and show the application's main form. This creates a problem because Sub Main executes whenever a new apartment is created. You must use a programming technique to load and show the main form only during your application's first execution of Sub Main. You can do this by loading an invisible form on application startup. When Sub Main runs, it can search through all loaded windows with a Win32 technique to see whether this form has been loaded. The sample application MTC1.vbp on this book's companion CD demonstrates one way to accomplish this.
The next problem you face is that you can't create additional apartments until the main apartment has been fully initialized. Therefore, you must wait until Sub Main completes before you can create any new objects in separate apartments. Sub Main won't complete until the form you are loading completes its Load method. This means that you can't create objects on separate threads during either of these methods. This isn't overly difficult to overcome, but it's a pain because many of us are used to creating our objects in Sub Main or in the Load event of our main form.
The first nontrivial problem surfaces when you want to run a task on your secondary thread without blocking the primary thread that's responsible for servicing the user interface. Currently COM lets you run method calls only in a synchronous manner. If you make a simple method call from your main form to an object in a different STA, the primary thread will be blocked until the call has completed. This totally defeats the purpose of creating a second thread.
Getting both threads to run at the same time requires an asynchronous method call. A future version of COM+ will modify the RPC layer to support true asynchronous calls. For now, however, all calls in COM are synchronous. A Visual Basic programmer must handcraft a "logical" asynchronous call by using the SetTimer function from the Win32 API. Again, the sample application MTC1.vbp shows how to set up an asynchronous call using this technique.
Finally, after you execute an asynchronous call from the main form to the secondary apartment, you must provide a way for the object to communicate with the main form. The object should notify the form of the task's update status while it's executing and ultimately the success or failure of the task. To do this, you can use either events or callback interfaces, as described in the previous chapter. The steps required for the asynchronous call are shown in Figure 7-11.
Figure 7-11. Running an asynchronous task is fairly complicated, even after you've created an object in a secondary apartment.