Working directly with Win32 threads and COM apartments is difficult and is far beyond the capabilities of the Visual Basic programming language. However, when Visual Basic 5 first shipped, many unfortunate programmers tried to directly call the CreateThread function in the Win32 API by using Visual Basic's new AddressOf operator. These programmers found that they could successfully call CreateThread but that their applications often died strange and mysterious deaths.
The problem with calling CreateThread is that it creates a new Win32 thread that knows nothing about COM or apartments. It's therefore illegal to use this thread to invoke a method on a COM object. This creates quite a problem because all Visual Basic objects, including forms and controls, are also COM objects. One of these objects can be directly accessed only by the thread running in the object's apartment. When the newly created thread tries to change a value in a text box, the application crashes. This is a pretty good reason for never using CreateThread directly from Visual Basic code.
A determined Visual Basic programmer with knowledge of COM's threading models might try to take things to the next level. You can make a thread enter an STA by calling a function in the COM library such as CoInitializeEx. However, once you get the thread into an apartment, the complexity increases. For instance, sometimes you must write code to marshal interface references (they're really interface pointers at this level) from one apartment to another. If you pass the interface references through a COM interface, the proxy and the stub are created automatically. However, if you pass them manually, the code requirements increase dramatically. You must make two complicated calls to the COM library. One call is needed to create a stub in the object's apartment, and a second call is needed to create a proxy in the caller's apartment. You also have to know enough about COM to decide when this code is necessary.
As you can see, you shouldn't go down this path unless you know an awful lot about the COM library and the OLE32 DLL. Moreover, anyone who can program at this level would prefer to program in C++ over Visual Basic because C++ is far more powerful when accessing the COM library directly. Now that you know where you shouldn't go, it's time to look at what you can do with Visual Basic. The remainder of this chapter will concentrate on what you can do in Visual Basic using reasonable techniques to achieve multithreading.
The easiest and most reliable way to create multithreaded servers with Visual Basic is to distribute your code as an in-process server. An in-process server doesn't require any code for creating or managing threads or apartments. This means that the application code for managing threads and apartments is handled by somebody else. When you create an ActiveX DLL project or an ActiveX Control project, your objects can take advantage of the threading used by the hosting applications. Sophisticated environments such as Microsoft Internet Explorer and MTS have been optimized to run apartment-threaded objects such as the ones you create with Visual Basic. All you have to do is make sure that your DLL project is marked for apartment threading.
Figure 7-6 shows the General tab of the Project Properties dialog box. When you create an ActiveX DLL or an ActiveX Control project, you can select the Apartment Threaded or Single Threaded threading model. Apartment Threaded is the default and is almost always the best choice. When you choose this setting, your CLSID's ThreadingModel attribute will be marked Apartment. An ActiveX DLL marked as Single Threaded can load objects only into the main STA. Environments such as Internet Explorer and MTS don't work well with components that use this legacy threading model. You should always mark your DLLs as apartment-threaded unless you have a good reason to do otherwise. Later in this chapter, we'll look at a special technique that involves using a single-threaded DLL.
Figure 7-6. ActiveX DLL and ActiveX Control projects have two possible settings for their threading model. An apartment-threaded server can load its objects into any available STA. A single-threaded server can load objects only into the application's main STA.
You should also consider marking your ActiveX DLL project to run with Unattended Execution if you plan to deploy it inside MTS or any other environment that runs nonvisual objects. The Unattended Execution option is for projects that will run without user interaction. Any run-time function such as MsgBox that normally results in user interaction is written to the Windows NT event log instead. Using this option guarantees that your code will never hang because of a modal dialog box that has been invoked by mistake.
MTS actually adds a twist to COM threading by adding yet another threading abstraction called an activity. (Chapter 9 covers MTS activities.) However, you'll still create DLLs that are marked to run as Apartment Threaded with Unattended Execution.
This chapter has made one specific recommendation to Visual Basic programmers: Never create a thread or an apartment on your own. However, you can create an ActiveX EXE project and let Visual Basic create and manage a set of apartments for you. When you build an ActiveX EXE with the appropriate settings, Visual Basic automatically builds the code into your server for creating and managing a set of STAs.
You can build an ActiveX EXE using one of three threading modes. Figure 7-7 shows where to configure these settings in the Project Properties dialog box. It seems at first that only two options are available, but you can build an ActiveX EXE using any of the following three settings:
Figure 7-7. You can set an ActiveX EXE project to one of three threading modes.
A new ActiveX EXE project is single-threaded by default, which means that all objects loaded into the server process will always run in the main STA. It also means that the server can run only one method call at a time. When an object executes a method, all other incoming requests are queued up on a processwide basis. This might be acceptable for an out-of-process server that has a single client, but when an out-of-process server has many connected clients, one client's method calls will block the calls of any other client. If a client invokes a long running method on an object in the server, all other client requests will be blocked until the method call has been completed. In many server designs, this lack of concurrency is unacceptable. You can use a different threading mode to make a server much more responsive in the presence of multiple clients.
If you set the threading mode to Thread Per Object, the server will automatically create a new apartment when an object is activated by a remote client. As you'll recall from Chapter 3, the client calls upon the SCM when it wants to create and connect to a new COM object. The client makes an activation request by calling into the COM library. The request is serviced by the SCM, which locates the server (loading it if necessary) and communicates with it to negotiate the creation of a new object. The Visual Basic team added a hook to create a new thread and apartment based on the project's threading mode. If you select Thread Per Object, the server creates a fresh, new apartment in which to run the new object.
It's important to understand exactly when new apartments are created. It's a bit tricky because the term thread-per-object isn't completely accurate. If an object running in the server process creates a second object with the New operator, the second object is loaded into the same apartment as its creator. Only when an external client makes an activation request is a new apartment created. This gives you a reasonable way to manage the apartments in your server process. It allows every connected client to run one or more objects in a private apartment. Each client can invoke methods with the guarantee that its calls won't be blocked by the calls of other clients. Furthermore, this threading mode makes it possible to create a set of objects on behalf of a single user that are all loaded into the same apartment. To do this, the client application should create the first object, and then this object should create all the others using the New operator. This is important because these objects can be bound to one another without the overhead of the proxy/stub layer.
One problem with thread-per-object threading is that it consumes a lot of resources when a large number of clients are connected. Remember that an invisible window is created for each STA, so load time and resource overhead are significant when the process has loaded up 50 or more apartments. You also reach a point of diminishing returns when you scale up the number of threads. When you reach this point, your server's actual throughput decreases as the amount of time required by the system to switch between threads increases. The Thread Pool option lets you balance your server's responsiveness with more efficient use of resources.
If you set the threading mode of an ActiveX EXE to a thread pool greater than 1, Visual Basic creates a new STA for each new object request until it reaches the thread pool capacity. If you have a thread pool of size 4, the server creates the first three objects in apartments 2, 3, and 4, respectively. Once the number of apartments is reached, Visual Basic uses a round-robin algorithm that matches new objects with existing apartments. The algorithm is reasonable but somewhat arbitrary. After placing the first three objects in apartments 2, 3 and 4, the server places objects into apartments with the sequence 4, 3, 2, main, 4, 3, 2, main, and so forth. You're better off not taking advantage of this knowledge because the Visual Basic team might decide to change the algorithm in a later version.
Using a thread pool always involves a compromise. You get more efficient use of resources, but you might experience problems with responsiveness. If two clients both activate objects that happen to be loaded into the same apartment, the method calls of one client will block the other. If your method calls are completed quickly, this might not be a problem. Method calls taking more than a second or two to complete might impose an unacceptable concurrency constraint.
Many veteran Visual Basic programmers get confused the first time they work with multithreaded servers because public variables defined in standard (.bas) modules aren't really global variables. These public variables are scoped at the apartment level. This changes things for programmers who are used to defining global, applicationwide variables in earlier versions of Visual Basic. When your component runs in a multithreaded environment, two objects see the same public standard-module data only when they are in the same apartment. Objects in separate apartments see different instances of these public variables. This creates random behavior in a server with a thread pool because of the arbitrary way in which objects are matched with apartments.
When the Visual Basic team added support for apartment threading, they had to decide how to handle the data defined in standard modules. If they decided to store this data in global memory, someone would have to be responsible for synchronizing access to it in the presence of multiple threads. As in the case of COM objects, the team felt that it was unacceptable to require any type of programmer-assisted synchronization. Instead, they opted to store the data defined in a standard module in thread-local storage (TLS), as shown in Figure 7-8. When the Visual Basic run time creates a new apartment, it also creates a fresh copy of every variable defined in a standard module in TLS. The thread in each new apartment also calls Sub Main, which lets you initialize the data in TLS just after it creates the new STA.
Figure 7-8. Any variable that you define in a standard module in an ActiveX DLL or an ActiveX EXE project is held in thread-local storage. This means that objects running in different apartments cannot share data.
The reasoning behind this decision is sound. Visual Basic programmers don't have to deal with the burden of synchronization. It's always taken care of behind the scenes. The cost of this convenience is that you can't use global data in the way that many Visual Basic programmers have in the past. You must either design multithreaded applications that don't depend on shared data or devise a fairly contrived scheme to share data among objects in different apartments.
One of the simplest ways to simulate global memory in an ActiveX EXE project requires the assistance of a second server project. You must create a single-threaded ActiveX DLL and reference it in the ActiveX EXE project. The trick is to define shared memory in the standard module of the single-threaded DLL project, as shown in Figure 7-9. All objects created from the DLL run in the main STA and therefore see the same copy of data defined in the standard module. Any object in any apartment can create an object from the DLL and call methods to read and write to this shared data. This solution isn't very elegant or efficient and is vulnerable to problems with locking and contention. Chapter 10 will revisit this problem of shared global data and provide a far more satisfying solution.
Figure 7-9. The easiest way to share data among objects in a multithreaded ActiveX EXE is to call upon a single-threaded ActiveX DLL. Any object in any apartment can create an object from the single-threaded DLL.