Objects, Interfaces, and Apartments
Clients want to call methods on objects. Objects simply want to expose their methods to clients. The fact that an object may have different concurrency constraints than those implied by the client's apartment is an implementation detail that the client should not care about. Similarly, if the object implementor chooses to deploy an object implementation only on a small set of host machines that are distinct from the host machine where the client program is located, this again is an implementation detail that the client should not care about. However, in either case, the object must reside in an apartment distinct from that of the client.
From a programming perspective, apartment membership is an interface pointer attribute, not an object attribute. When an interface pointer is returned from a COM API call or from a method invocation, the thread that invoked the API call or method determines which apartment the resultant interface pointer belongs to. If the call returns a pointer to the actual object, then the object itself resides in the calling thread's apartment. Often, the object cannot reside in the caller's apartment, either because the object already exists in a different process or host machine or because the concurrency requirements of the object are incompatible with the client's apartment. In these cases, the client receives a pointer to a proxy.
In COM, a proxy is an object that is semantically identical to an object in another apartment. In a sense, a proxy represents another object's identity in a different apartment. A proxy exposes the same set of interfaces as the object it represents, however the proxy's implementation of each of the interface's methods simply forwards the calls to the object, ensuring that the object's methods always execute in the object's apartment. Irrespective of whether the client receives a pointer to an object or a pointer to a proxy, any interface pointer that a client receives from an API call or a method call is valid for all threads in the caller's apartment.
Object implementors decide the types of apartments in which their objects can execute. As is discussed in Chapter 6, out-of-process servers explicitly decide their apartment type by calling CoInitializeEx with the appropriate parameter. For in-process servers, a different approach is needed, as the client will already have called CoInitializeEx by the time the object is created. To allow in-process servers to control their apartment type, COM allows each CLSID to have its own distinct threading model that is advertised in the local registry using the ThreadingModel named value:
[HKCR\CLSID\{96556310-D779-11d0-8C4F-0080C73925BA}\InprocServer32]
@="C:\racer.dll"
ThreadingModel="Free"
Each CLSID in a DLL can have its own distinct ThreadingModel. Under Windows NT 4.0, COM allows four possible ThreadingModels for a CLSID. ThreadingModel="Both" indicates that the class can execute in either an MTA or an STA. ThreadingModel="Free" indicates that the class can execute only in an MTA. ThreadingModel="Apartment" indicates that the class can execute only in an STA. The absence of a ThreadingModel value implies that the class can run only on the main STA. The main STA is defined as the first STA to be initialized in the process.
If the client's apartment is compatible with the CLSID's threading model, then all in-process activation requests for that CLSID will instantiate the object directly in the apartment of the client. This is by far the most efficient scenario, as no intermediate proxy is needed.3 If the client's apartment is incompatible with the CLSID's threading model, then in-process activation requests for that CLSID will force COM to silently instantiate the object in a distinct apartment, and a proxy will be returned to the client. In the case of STA-based clients activating ThreadingModel="Free" classes, the class object (and subsequent instances) will execute in the MTA. In the case of MTA-based clients activating ThreadingModel="Apartment" classes, the class object (and subsequent instances) will execute in a COM-created STA. In the case of any type of client activating main STA-based classes, the class object (and subsequent instances) must execute in a main STA of the process. If the client happens to be the main STA thread, then the object will be accessed directly. Otherwise, the client will get back a proxy. If no STAs exist in the process (that is, if no threads have called CoInitializeEx with the COINIT_APARTMENTTHREADED flag), then COM will create a new STA to act as the main STA for the process.
Class implementors that do not mark a threading model for their classes can largely ignore threading issues, as their DLL will be accessed from only one thread, the main STA thread. Implementors that mark their classes as supporting any explicit threading model are implicitly indicating that multiple apartments in a process (which implies the potential for multiple threads) may each contain instances of the class. Because of this, the implementor must protect any resources that are shared by more than one instance of the class against concurrent access. This means that all global and static variables must be protected using an appropriate thread synchronization primitive. For a COM in-process server, the global lock count that keeps track of the server's lifetime must be protected using InterlockedIncrement/InterlockedDecrement, as demonstrated in Chapter 3. Any other server-specific state needs to be protected as well.
Implementors that mark their classes as ThreadingModel="Apartment" are stating that their instances must be accessed from only one thread for the lifetime of the object. This implies that there is no need to protect instance state, only the state that is shared by multiple instances of the class, as mentioned previously. Implementors that mark their classes as either ThreadingModel="Free" or ThreadingModel="Both" are making the statement that instances of their class may run in the MTA, which means that a single instance of the class may be accessed concurrently. Because of this, implementors must protect all resources that are used by a single instance against concurrent access. This applies not only to shared static variables but also to instance data members. For heap-based objects, this implies that the reference count data member must be protected using InterlockedIncrement/InterlockedDecrement, as demonstrated in Chapter 2. Any other class-specific instance state needs to be protected as well.
At first glance, it is less than obvious why ThreadingModel="Free" exists, because the requirements of running in an MTA are often seen as a superset of the requirements for STA compatibility. If an object implementor plans on creating worker threads that will need access to the object, it is highly advantageous to prevent the object from being created in an STA. This is because the worker threads cannot enter the STA where the object lives and therefore must run in a different apartment. If a class is marked ThreadingModel= "Both" and an activation request is made from an STA-based thread, the object will live in an STA. This means that the worker threads (which will run in the MTA) must access the object using interapartment method calls, which are considerably less efficient than intraapartment method invocation. However, if the class is marked ThreadingModel="Free," then any STA-based activation requests will force the new instance to be created in the MTA, where any worker threads could access the object directly. This means that when the STA-based client invokes methods on the object, the performance will be diminished; however, the worker threads will experience much better performance. This is a reasonable trade-off if the object will be accessed by the worker threads more often than by the actual STA-based client. It is tempting simply to relax the rules of COM and note that some objects may be directly accessed from more than one apartment without causing faults. However, in the general case, this is not true, especially for objects that use other objects to perform their work.
3 The performance cost of an interapartment method invocation can potentially be thousands of times greater than that of an intraapartment method call due to thread-switching overhead.