Click Here to Install Silverlight*
United StatesChange|All Microsoft Sites
MSDN
|Developer Centers|Library|Downloads|Code Center|Subscriptions|MSDN Worldwide
Search for


Advanced Search
MSDN Home > MSJ > May 1998
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
Paris, France
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.
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 1 Project Properties for in-process servers


If you select Apartment Threaded for your in-process server, Visual Basic adds the ThreadingModel=Apartment entry for each class that your server exports. This tells COM to go ahead and create your objects in the apartment of the caller, provided the calling thread is not executing inside the one multithreaded apartment (MTA) in the process (in which case COM will create or find a COM-managed STA to house your object). This means there will be no proxy and your methods will execute directly on the client's thread. Since it is illegal for your STA client to allow any other threads to touch your interface pointers directly, your object is still protected against concurrent access.
Figure 2 shows the corresponding dialog for building out-of-process servers. Note that for an out-of-process server, the threading behavior of the client is more or less orthogonal to how you decide to handle threading. Acknowledging this, Visual Basic allows you to be fairly proactive in terms of managing how your objects will relate to threads. In the lower-right corner of the dialog there are again two options for Threading Model, but these are very different from the in-process case.

Figure 2 Project Properties for out-of-process servers
Figure 2 Project Properties for out-of-process servers


The simplest option to understand is Thread Per Object. Selecting Thread Per Object informs the Visual Basic plumbing to create a new thread (in a new, private STA) for each CoCreateInstance/CreateObject request. Visual Basic then forwards the activation request to the newly created thread/apartment and returns a proxy to the client. The new thread will live for the lifetime of the object and will be destroyed once the object's Terminate method executes. For a small number of clients/objects, Thread Per Object provides each client with a dedicated thread in the server that exists exclusively to service the needs of that client. For a large number of clients/objects, Thread Per Object causes the server to consume an inordinate amount of resources and will likely cause a performance meltdown.
Visual Basic offers the Thread Pool option to reduce the number of threads consumed by a single server process. As Figure 2 shows, when selecting the Thread Pool option, the developer can determine the maximum number of threads that will be created to service new objects. As long as the total number of objects is below this number, the Thread Pool option behaves like Thread Per Object. As with Thread Per Object, threads (and apartments) are not allocated until they are needed. However, once the number of objects exceeds the maximum thread count, Visual Basic starts to forward new activation requests to one of the existing threads/apartments in the pool. This means that several clients may share a server thread/apartment, which is both good (because it consumes less resources) and bad (because one client's request can block another's). Selecting a maximum thread count of 1 puts your code into Visual Basic 4.0 compatibility mode and forces all of your objects to reside in the main thread/apartment of the server process.
So, why do you need to know this? Depending on which option you choose, the semantics of the language change subtly. In particular, the scope of global and static variables has a slightly different meaning based on the selected threading option.
Recall that an object that resides in a single-threaded apartment will have all method calls serialized by COM automatically. However, if two objects of the same class reside in two different apartments, a method call to one object can execute concurrently with method calls on other objects of the same class. Provided that these objects only access per-instance state, this is not a problem. In fact, allowing multiple methods to execute concurrently generally increases the throughput, scalability, and responsiveness of the system. However, it is common for an object to use resources that are shared between two or more instances of the same class. Therein lies the problem.
Visual Basic allows programmers to declare global variables that are visible to all objects in the current project. Because Visual Basic 5.0 allows objects to reside in multiple apartments, the potential for unprotected concurrent access to these global variables would exist unless the Visual Basic runtime took some action. Faced with this problem, the Visual Basic product team decided it was a bad idea to expose explicit locking primitives to the average developer, so the solution needed to be as transparent as possible.
One approach would have been to wrap all global variable accesses behind a Win32 critical section. This would have meant the following Visual Basic code


 Public g_nSharedSum as Long ' in Module1.bas
 Public g_nAccesses  as Long ' in Module1.bas
 Private m_nSum      as Long ' in Class1.cls
 Sub Add(ByVal n as Long)
   g_nSharedSum = g_nSharedSum + n
   m_nSum = m_nSum + n
   g_nAccesses = g_nAccesses + 1
 End Sub
would translate to the following C++ pseudo-code:

 CRITICAL_SECTION g_csGlobals; // module-wide lock
 long g_nSharedSum = 0, g_nAccesses = 0;
 STDMETHODIMP Class1::Add(long n) {
   EnterCriticalSection(&g_csGlobals);
   g_nSharedSum = g_nSharedSum + n;
   LeaveCriticalSection(&g_csGlobals);
   m_nSum = m_nSum + n;
   EnterCriticalSection(&g_csGlobals);
   g_nAccesses = g_nAccesses + 1;
   LeaveCriticalSection(&g_csGlobals);
   return S_OK;
 }
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:

 STDMETHODIMP Class1::Add(long n) {
   EnterCriticalSection(&g_csGlobals);
   g_nSharedSum = g_nSharedSum + n;
   m_nSum = m_nSum + n;
   g_nAccesses = g_nAccesses + 1;
   LeaveCriticalSection(&g_csGlobals);
   return S_OK;
 }
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

 Public g_nSharedSum as Long
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

 Public g_nSharedSum as Long
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:

 Public g_rgn(1000000) as Long
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 these 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 them with other components in MTS. Figure 4 shows how these 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:

 Public g_nSharedSum as Long ' in Module1.bas
 Public g_nAccesses  as Long ' in Module1.bas
 Private m_nSum      as Long ' in Class1.cls
 Sub Add(ByVal n as Long)
   g_nSharedSum = g_nSharedSum + n
   g_nAccesses = g_nAccesses + 1
   m_nSum = m_nSum + n
 End Sub
Since the two variables, g_nSharedSum and g_nAccesses, will be thread-specific, you cannot actually declare them 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:

 Public g_nSharedSum as Long ' process-wide.bas
 Public g_nAccesses  as Long ' process-wide.bas
 Sub SharedAdd(ByVal n as Long) ' SharedState.cls
   g_nSharedSum = g_nSharedSum + n
   g_nAccesses = g_nAccesses + 1
 End Sub
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:

 Private m_nSum as Long ' in Class1.cls
 Sub Add(ByVal n as Long)
   Dim ss as New SharedState 'loads DLL in main STA
   ss.SharedAdd n ' passes control to main STA
   m_nSum = m_nSum + n
 End Sub
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
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 these objects from your Visual Basic object is simple, assuming you have implemented the well-known methods OnStartPage and OnEndPage as follows:

 Private m_asp As ASPTypeLibrary.IScriptingContext
 Sub OnStartPage(ByVal pUnk As IUnknown)
   Set m_asp = pUnk
 End Sub
 Sub OnEndPage( )
   Set m_asp = Nothing
 End Sub
You could easily use the Session object to store your global variables as named properties of the session:

 Private m_nSum   as Long ' in Class1.cls
 Sub Add(ByVal n as Long)
   If (m_asp.Session("g_nSharedSum") = "") Then
     Rem Init first time through for this session
     m_asp.Session("g_nSharedSum") = 0
     m_asp.Session("g_nAccesses") = 0
   End If
   m_asp.Session("g_nSharedSum") _
            = m_asp.Session("g_nSharedSum") + n
   m_asp.Session("g_nAccesses") _ 
            = m_asp.Session("g_nAccesses") + 1
   m_nSum = m_nSum + n
 End Sub
If your global variables need to be truly global (which is the whole point of this discussion), you could instead store them as properties of the Application object:

 Private m_nSum   as Long ' in Class1.cls
 Sub Add(ByVal n as Long)
   m_asp.Application.Lock
   If (m_asp.Application("g_nSharedSum") = "") Then
     Rem Init first time through for this session
     m_asp.Application("g_nSharedSum") = 0
     m_asp.Application("g_nAccesses") = 0
   End If
   m_asp.Application("g_nSharedSum") _
            = m_asp.Application("g_nSharedSum") + n
   m_asp.Application("g_nAccesses") _ 
            = m_asp.Application("g_nAccesses") + 1
   m_asp.Application.Unlock
   m_nSum = m_nSum + n
 End Sub
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:

   interface ISharedPropertyGroupManager : IDispatch{ 
   // Create/open a named lock
   HRESULT CreatePropertyGroup(
       [in] BSTR Name, 
       [in, out] long* dwIsoMode, 
       [in, out] long* dwRelMode, 
       [out] VARIANT_BOOL* fExists, 
       [out, retval] ISharedPropertyGroup** ppspg);
   // Open an existing named lock
   HRESULT Group(
       [in] BSTR Name, 
       [out, retval] ISharedPropertyGroup** ppspg);
   // Enumerate all locks
   HRESULT _NewEnum([out, retval] IUnknown** ppe);
 }
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
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:

   interface ISharedPropertyGroup : IDispatch {
   HRESULT CreatePropertyByPosition(
             [in] int Index, 
             [out] VARIANT_BOOL* fExists, 
             [out, retval] ISharedProperty** ppsp);
   HRESULT PropertyByPosition(
             [in] int Index, 
             [out, retval] ISharedProperty** ppsp);
   HRESULT CreateProperty(
             [in] BSTR Name, 
             [out] VARIANT_BOOL* fExists, 
             [out, retval] ISharedProperty** ppsp);
   HRESULT Property(
             [in] BSTR Name, 
             [out, retval] ISharedProperty** ppsp);
 }
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:

 interface ISharedProperty : IDispatch {
   [propget] HRESULT Value([out,retval] VARIANT*p);
   [propput] HRESULT Value([in] VARIANT pVal);
 };
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 them in the Add method is trivial:

 Sub Add(ByVal n as Long)
   m_nSum = m_nSum + n
   Rem acquire lock by touching 1st property
   m_spnSharedSum = m_spnSharedSum + n
   m_spnAccesses = m_spnAccesses + 1
  Rem release lock by returning from method
 End Sub
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:

 Sub CreateAndStore()
   Dim objref as IBob
   Set objref = m_asp.Server.CreateObject("Bob")
   Set m_asp.Application("HiBob") = objref
 End Sub
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 SPM—provided 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:

 Sub CreateAndStoreIt(ByVal sp as ISharedProperty)
   Rem create an object in another process
   Dim itf as IOutOfProcInterface
   Set itf = CreateObject("SomeOutOfProcClass")
   Rem convert proxy to a GIT cookie
   Dim dwGIT as Long
   dwGIT = GITHelp.RegisterInterfaceInGlobal(itf)
   Rem store GIT cookie as long in SPM
   sp.Value = dwGIT
 End Sub
Because GIT cookies can be unmarshaled numerous times, the cookie could be read and used as many times as it is needed:

 Sub UseIt(ByVal sp as ISharedProperty)
   Rem read GIT cookie from SPM
   Dim dwGIT as Long
   dwGIT = sp.Value
   Rem unmarshal a new proxy in this apartment
   Dim itf as IOutOfProcInterface
   Set itf = GITHelp.GetInterfaceFromGlobal(dwGIT)
   Rem use proxy
   itf.SomeInterestingMethod
 End Sub
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:
  • Your object is the equivalent of an apartment-neutral object à la the freethreaded marshaler.
  • Any object references your object holds as data members have to be stored as apartment-neutral GIT cookies.
  • You cannot touch any of your Visual Basic VM-managed global variables, since they are stored in thread-specific memory.
  • The Visual Basic VM is not prepared for this type of access. In particular, it is completely unsupported, likely to break miserably, and completely voids your warranty and possibly your license agreement. (You did click the "I Agree" button, didn't you?)
In summary, if you work in Visual Basic, you have two choices. You could create single-threaded components and suffer the (potentially severe) performance penalties. Or, you could create ThreadingModel=Apartment components and eschew the Visual Basic global variables in favor of either the MTS SPM, ASP Application or Session objects, or (worst case) a secondary single-threaded DLL.

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. Get it at your local newsstand, or better yet, subscribe.

© 1998 Microsoft Corporation. All rights reserved.
Terms of Use
.

© 2016 Microsoft Corporation. All rights reserved. Contact Us |Terms of Use |Trademarks |Privacy & Cookies
Microsoft