May 1998
Don Box is a co-founder of DevelopMentor where he manages the COM curriculum. Don is currently breathing deep sighs of relief as his new book, Essential COM (Addison-Wesley), is finally complete. Don can be reached at http://www.develop.com/dbox. |
Q Since becoming a COM developer, I have become fairly language-agnostic. I've recently switched to Visual Basic®, but I've noticed some really weird behavior that causes my objects to break when accessed from Microsoft® Transaction Server (MTS), Microsoft Internet Explorer, or Active Server Pages (ASP). In particular, my global variables seem to intermittently lose their values between method calls. Also, if my application runs long enough, it eventually runs out of memory and crashes. This is a big problem since I am implementing a Weblication for use by thousands of Windows CE clients (Handheld PCs, Palm-size PCs, and Auto PCs) and need extremely high availability. I've tried reinstalling the OS, tools, option packs and all relevant service packs (and hot fixes), yet my problem persists. (Changing the batteries in my Handheld PC had no effect either.) Help!
Amy Pattison
A I've resorted to the "rebuild my world starting with FDISK" approach to debugging before, so I can feel your pain. Unfortunately, your problem has nothing to do with malfunctioning development tools or colliding service packs. It has to do with a threading decision made by the Visual Basic development team that baffles many a programmer. To understand the problem, and find a solution, let's first look at how Visual Basic works.
Paris, France Visual Basic 4.0 was easy to understand. It was fairly oblivious to threading. Visual Basic 4.0 out-of-process servers generated single-threaded applications where all objects lived on a single thread. Visual Basic 4.0 in-process servers specifically did not register a ThreadingModel entry (sometimes referred to as ThreadingModel=Single), so all objects lived on the main thread of the client application. This solution worked great, at least semantically, since COM ensured that no concurrent access happened on your objects. This meant that you could live the carefree Visual Basic lifestyle to its fullest without concern for concurrency, locks, mutexes, or any other Win32® thread grunge that usually has nothing to do with the domain problem you are trying to solve. And then along came Visual Basic 5.0. Visual Basic 5.0 has actually had several service packs, but I will restrict my discussion to the most current version (as of February 1998): Service Pack 3. Visual Basic 5.0 gives you control over how your objects will relate to COM apartments. Figure 1 shows the Project Properties dialog for in-process servers. Note that there are two options for Threading Model in the lower-right corner: Single Threaded and Apartment Threaded. Selecting Single Threaded puts your code into Visual Basic 4.0 compatibility mode and forces all of your objects to reside in the main apartment of the client process. The main apartment is simply the first single-threaded apartment (STA) to be initialized in a process. If the client tries to create one of your objects from any other apartment, COM will automatically return a proxy to the caller. This proxy ensures that all access to your object is serialized by forwarding method requests to the main apartment's thread. This can have a severe impact on performance, as two thread switches are required for every method call. |
Figure 1 Project Properties for in-process servers |
|
Figure 2 Project Properties for out-of-process servers |
|
|
would translate to the following C++ pseudo-code: |
|
By wrapping each statement that touches a global variable in a critical section, the runtime could ensure that each statement executed atomically. But this translation leaves the aggregate state of the global variables inconsistent since one thread could be preempted between the two accesses.
A safer technique would have been for the runtime to wrap the entire method inside a single critical section: |
|
This would ensure that both variables would be updated atomically. Unfortunately, acquiring a process-wide lock in every method would have a nontrivial performance impact as all method calls in the server would need to be serialized. The compiler could help a bit by being more aggressive in terms of shortening the duration of the lock, but this was not the approach taken by the Visual Basic product group.
To avoid the issue of locking entirely, the Visual Basic team solved the global variable problem by changing the semantics of global variables. In Visual Basic 4.0 and earlier, global variables were visible anywhere within the module. If your .BAS file contained the declaration |
|
any part of your code would see the same value for this variable. In Visual Basic 4.0 and earlier, the scope of a global variable is a process. In Visual Basic 5.0, it's an apartment, not a process. This means each thread gets its own private copy of all global variables. Because global variables are thread-specific, no locking is needed and access is extremely fast.
For a single-threaded in-process server or an out-of-process server with a thread pool limit of one thread, there will be only one copy of your global variables per process (since at most only one thread will ever touch any of your objects), as shown in Figure 3. For an apartment threaded in-process server or an out-of-process server with a thread pool limit greater than one thread (or thread per object), you will have n copies of your global variables, where n is the number of threads holding objects in your server. If your .BAS file contained the following declaration |
|
each thread would have its own private copy of the variable. If two objects in the same apartment were to access it, they would see each other's changes. If two objects in different apartments were to access it, each would be accessing its own thread-specific version and would not see the Other's changes. This changes the semantics of global variables considerably!
One additional downside to putting your global variables in thread-specific storage is that memory must be allocated for each thread. Consider the following declaration from a .BAS file: |
|
This line causes the Visual Basic VM to allocate an additional 4MB per thread. This is not all that surprising given the new semantics of global variables, but you might not expect that the Visual Basic VM never frees the memory. (OK, the memory is reclaimed when the process exits.) This means that using global variables and multi-apartment programming with Visual Basic is a dangerous proposition.
Given the hazards of global variables, what options are available? The simplest option is to never generate apartment threaded DLLs or to allow thread pools of greater than one thread in out-of-process servers. In essence, this means reverting back to Visual Basic 4.0-style behavior. However, if you are writing in-process servers that will run inside of ASP, MTS, or Internet Explorer, you will experience a nontrivial performance hit as each of the Se environments creates multiple STA threads, each of which may want to create instances of your class. Worse yet, MTS will not allow single-threaded and apartment threaded objects to coexist in the same activity, so you may in fact be forced to use apartment threaded objects if you are mixing the M with other components in MTS. Figure 4 shows how the Se three environments relate to apartments and Visual Basic global variables in apartment threaded DLLs. Assuming that you opt to support multiple apartments either using thread pools or apartment threaded DLLs for performance reasons, you may still need to use process-wide global variables. If this is the case, you basically have three options, depending on where you are deploying your server. The most universal technique also has the worst performance. Suppose you need to implement the following code fragment in an apartment threaded DLL: |
|
Since the two variables, g_nSharedSum and g_nAccesses, will be thread-specific, you cannot actually declare the M in your .BAS file. However, if you create a second Visual Basic project that generates a single-threaded DLL, any global variables declared in that project would be process-wide, not thread-specific. Given this behavior, you could write a new COM class (SharedState) that would be exported from the single-threaded DLL: |
|
Given this class, the apartment threaded component (which is built in a separate DLL) could use the single-threaded component to access the process-wide shared state: |
|
Because the class that operates on the shared state is marked ThreadingModel=<none>, COM will automatically force the activation call into the main apartment of the process irrespective of which thread creates the Object (see Figure 5). This ensures that only one copy of the global variables will exist, and that all calls will be serialized to the main thread of the application.
|
Figure 5 Globals Using a Secondary Single-threaded DLL |
While this technique works in virtually all environments (ASP, MTS, and Internet Explorer), it has an unfortunate side effect: all accesses to the shared state require a thread switch, which can kill performance even when there is little contention for the shared variables. If you are working in Internet Explorer, there aren't any other options that are easily accessible from pure Visual Basic. If you are working in ASP or MTS, you are in luck.
The ASP object model provides two global objects that allow you to add additional application-specific named properties. The Session object is created by ASP for each client session and lives beyond the scope of a single ASP page. Session objects go away either after some configurable period of inactivity or when an ASP script or component calls the Session object's Abandon method. The Application object is created by an ASP-based application when the first client session begins, and it lives at least as long as one session is still active. There is only one Application object per ASP-based application, so it can be used to share variables across session boundaries. Accessing the Se objects from your Visual Basic object is simple, assuming you have implemented the well-known methods OnStartPage and OnEndPage as follows: |
|
You could easily use the Session object to store your global variables as named properties of the session: |
|
If your global variables need to be truly global (which is the whole point of this discussion), you could instead store the M as properties of the Application object: |
|
You should note that because the application is visible to multiple client sessions simultaneously, the ASP component must explicitly lock and unlock access to the Object to ensure that the updates happen atomically.
Using the ASP Application object to implement cross-apartment global variables works great if only one ASP-based application will ever use your component. However, what if you need your global variable to span multiple ASP-based applications, or you are writing objects that don't have anything to do with ASP? Enter MTS. As discussed in my March 1998 column, MTS has a rich programming model and runtime environment for managing state in a distributed object environment. For managing transient, process-wide memory (referred to as Level 3 state in that article), MTS provides the Shared Property Manager (SPM). The SPM allows MTS-based objects to share nonpersistent, nontransactional state in a thread-safe fashion. As shown in Figure 6, the SPM allows objects to create named locks called property groups. Each property group contains an MTS-aware lock and a collection of named properties. MTS objects access the SPM via the Shared Property Group Manager, which exposes the ISharedPropertyGroupManager interface: |
|
As with the Win32 API, SPM create methods can be used as an open call as well. The fExists parameter indicates whether the create call created a new group/lock or opened an existing one.
|
Figure 6 MTS Shared Property Manager |
Besides a unique name, two other pieces of information are needed when creating a new SPM property group: the isolation mode and the release mode. The isolation mode controls how long the lock is held once a property in the group is accessed. Indicating LockSetGet specifies that the lock can be released as soon as the individual property has been read or written. Indicating LockMethod specifies that the group-wide lock must be held until the current method returns control. Using LockMethod guarantees that multiple properties in a group can be updated atomically. However, it can reduce the potential concurrency as the lock may be held longer than is technically required.
The second piece of information, the release mode, indicates how long the lock (and its protected properties) will remain valid. Indicating a release mode of Standard tells the SPM to destroy the lock and its properties when the last object releases its reference to the property group. Indicating a release mode of Process tells the SPM to keep the lock and its properties around for the duration of the process lifetime. Each named property group exposes its properties via the ISharedPropertyGroup interface: |
|
As shown above, properties can be accessed either by a unique text-based name or by a zero-based index. Again, the create methods can be used to open existing properties. Access to each property in a group is via a simple read-write interface, ISharedProperty: |
|
The underlying group-wide lock is not acquired until the property is accessed, so you can safely open references to specific properties in an object's initialization routine.
Porting the previously shown Add method to use the SPM is fairly straightforward. Assuming that the Add method will be called multiple times per object, it is probably worthwhile to open references to the two shared properties during the Object's initialization routine (see Figure 7). Note that the initialization is postponed until the call to IObjectControl::Activate. This is standard form for an MTS-based component, as the constructor of the class does not have access to MTS-specific context. Once the references to the two properties have been initialized, accessing the M in the Add method is trivial: |
|
Note that the group-wide lock will not be acquired until the first property access. Because the property group was initialized to use method-level locking, the lock is held until the Add method returns, ensuring that the two properties will not be accessible until both have been updated.
It is interesting to note that MTS shared properties as well as ASP Session and Application properties are of type VARIANT. This allows you to store numeric types, strings, currency and dates equally well. However, VARIANTs also support object references, and that is where the MTS SPM differs from ASP. You can attempt to write an object reference to an ASP Application property as follows: |
|
Unless the Object you are referring to is a proxy or is apartment-neutral (that is, uses the freethreaded marshaler), ASP will fail to accept the Object reference as an application-wide property. This is because the Object would be tied to the current STA thread and all subsequent method invocations would need to be dispatched to the thread currently running the ASP script. This would severely impact overall performance as well as potentially break the ASP thread pooling strategy. The net result is that ASP Application properties cannot hold references to in-process objects implemented in Visual Basic. No such limitation applies to the ASP Session object.
In the case of the MTS SPM, the current implementation simply AddRefs your object reference in the storing apartment and happily hands out cross-apartment references to objects in other activities or apartments. This is in violation of the COM apartment rules and was probably an oversight. Since the SPM is visible to multiple apartments in a process, it arguably should have used a strategy similar to the ASP Application object. While it is dangerous to store raw interface pointers in the SPM based on its current implementation, it is reasonable to store Global Interface Table (GIT) cookies in the SPMprovided you know that you are referring either to a proxy or an apartment-neutral object. (See my September 1997 column for more information on the GIT.) While the GIT cannot be accessed directly from Visual Basic, you can download a Visual Basic-friendly wrapper from my Web site at http://www.develop.com/dbox/com/vbgit. Given this wrapper, you could store a proxy in the SPM as a GIT cookie: |
|
Because GIT cookies can be unmarshaled numerous times, the cookie could be read and used as many times as it is needed: |
|
This is essentially what the ASP Application object does when you store an object reference as an application-
wide property.
You may be tempted to just shove a raw reference to your Visual Basic-produced object into the SPM and take advantage of the SPM serialization to protect your object from concurrent access. While technically all access to the Object would be serialized, it would also be issued from the wrong apartment and thread, meaning:
Have a question about programming with ActiveX or COM? Send your questions via email to Don Box: dbox@develop.com or http://www.develop.com/dbox
From the May 1998 issue of Microsoft Systems Journal.
|