|< BACK  NEXT >|
A component s implementors decide the threading strategy (the apartment type) to be used by the component. However, an approach based on the component calling CoInitializeEx cannot be used. A component is built as an in-process server (a DLL) the client will have already called CoInitializeEx by the time an object gets created.
The approach taken by COM+ was that each COM class defines its threading model in the Windows registry key. Recall from Chapter 5 that the COM+ Catalog Manager controls the Windows registry key, HKEY_CLASSES_ROOT\CLSID, and that COM+ expects the implementors to add the requisite registry entries in the export function DllRegisterServer and to clean up the entries in the export function DllUnregisterServer.
The threading model for a CLSID is specified under the following registry value:
There are five possible values for the ThreadingModel registry entry, as follows:
ThreadingModel="Apartment" indicates that the object can be created (and executed) only in an STA.
ThreadingModel="Free" indicates that the object can be created (and executed) only in an MTA.
ThreadingModel="Both" indicates that the object can be created either in an STA or MTA. COM+ uses the apartment type of the client to create the object. Of course, the object will execute only in the apartment it is created in.
ThreadingModel="Neutral" indicates that the object can be created (and executed) only in a TNA.
The absence of a ThreadingModel entry represents a legacy case. It implies that the object can run only on the main STA. The main STA is defined as the first STA to be initialized in the process.
The interaction between apartments, context, objects, and threads has been a source of tremendous confusion among the COM community. Let me see if I can clear up some of the confusion:
An apartment is nothing but a group of contexts sharing the same concurrency requirements.
An object never enters or leaves an apartment. The object is created in an apartment (more specifically, in a context within an apartment) based on the ThreadingModel registry entry. The object lives and dies in the apartment.
A thread enters and leaves an apartment. A thread can enter only one apartment at a time. Once a thread enters an apartment, it gets associated with the default context for the apartment (remap="default contexts are covered in Chapter 5).
The TNA is a special apartment that has meaning only for the objects. A thread can never enter or leave a TNA.
If the client s apartment is not compatible with that of the component s threading model, COM+ silently creates the object in a compatible apartment and returns a proxy to the client.
If the object s apartment is either an STA or an MTA, a cross-apartment call will result in a thread switch. If the object s apartment is a TNA, the thread switch is not needed for the cross-apartment call.
If the client s apartment is compatible with that of the component, there are two cases to be addressed.
If the client s context is the same as that of the created object, the client gets a raw reference to the object.
If the contexts are not the same, the client gets a proxy. However, there is no thread switch involved during cross-context calls, as the contexts are part of the same apartment.
If an object spawns a thread (using CreateThread, for example), the resulting thread does not inherit the context of the parent object. The thread can enter an apartment, thereby getting the default context for the apartment.
Component implementors that do not mark a threading model for their classes can ignore threading issues, as their DLL will be accessed from only one thread, the main thread.
Implementors that mark their classes as Apartment are indicating that any object that is created can be accessed only from one thread for the lifetime of the object. This implies that there is no need to protect the state of an individual instance. However, it is possible to have multiple instances of the COM class in the same process (on different threads, of course). Therefore, if these multiple instances share some resource, the resource still needs to be protected from multiple accesses.
ThreadingModel = "Both" implies that the object should be created in the same apartment as that of the client. A client, irrespective of its apartment state, does not require a thread switch while making a method call on the object. This by far is the most efficient case. However, if the object interface is marshaled and passed to another apartment, a cross-apartment call will still result in a thread switch.
ThreadingModel = "Neutral" is similar to "both" in the sense that a method call never requires a thread switch. The added advantage is that if the object interface is marshaled and passed to another apartment, a cross-apartment call also does not require a thread switch. Although a case can be made that ThreadingModel="Both" is a bit more efficient than "Neutral" under some circumstances (the client gets a raw pointer, not the proxy), the performance difference is negligible. Moreover, an implementor can specify additional synchronization settings with ThreadingModel="Neutral" as we will see later. Hence, under COM+, this is the preferred threading model for components that do not require any thread-affinity.
ThreadingModel="Free" results in some code simplification under classic COM. Under classic COM, there is no need to explicitly marshal a reference between two threads within the same apartment. If a class is marked as "Free", any instance created for the class resides in the MTA and could be accessed by a secondary MTA thread directly, even if the instantiation came from an STA-based client. Under COM+, however, two threads within an apartment may belong to different contexts, and passing a reference from one thread to another may still require explicit marshaling. Therefore, ThreadingModel="Neutral" is preferred over ThreadingModel="Free" under COM+.
If you do not wish to use any of the COM+ Services for a component and would like to avoid explicit marshaling between the threads, you can install the component as non-configured and set the ThreadingModel to "Free".
Earlier, I indicated that the preferred threading model for components that do not require any thread affinity is Neutral. This configuration setting on the component forces COM+ to create the object in a TNA.
As multiple threads can concurrently access a TNA object, the implementors still need to deal with multithreading issues and explicitly use defensive multithreaded programming techniques.
But what about those developers who would rather focus on business needs and not worry about multithreaded programming?
To provide synchronized access to an object, COM+ provides a generic solution called activity-based synchronization.
An activity is a collection of one or more contexts that share concurrency characteristics. An activity may cross processes as well as machine boundaries. Within an activity, concurrent calls from multiple threads are not allowed, that is, COM+ provides intrinsic call serialization across all the contexts in an activity.
As shown in Figure 6.2, a context within a process may at most belong to one activity, but many contexts may not belong to an activity at all. Contexts that do not belong to an activity (such as the default context of an apartment) get no intrinsic call serialization, that is, any thread in the context apartment can enter the context at any time. Of course, if the context belongs to an STA, then no more than one thread will ever access the context.
At first glance, an activity and an STA appear to be the same thing. However, an activity is far more powerful than an STA. It can span multiple processes and machines although, as you will see later, COM+ weakens the concurrency guarantees when an activity spans multiple processes.
An object can reside in any of the following activities:
Creator s activity
Component implementors specify the activity the component should belong to by means of a configurable attribute called synchronization. Figure 6.3 shows various synchronization values that can be set on a component.
Following is an explanation of the various synchronization settings:
Disabled: Any synchronization-related overhead for the component is eliminated.
Not Supported: The object has no interest in either starting its own activity or being part of its creator s activity.
Supported: The object doesn t care for the presence or absence of its creator s activity. If the creator belongs to an activity, the object will enter this activity. Otherwise, no new activity will be created.
Required: The object has to be part of an activity. If its creator belongs to an activity, the object will enter this activity. Otherwise, the object will start a new activity.
Requires New: The object will always start a new activity, independent of its creator s activity status.
Note that the ThreadingModel attribute of a component dictates which threads in an apartment can dispatch calls to an object, whereas the synchronization attribute dictates whena thread can dispatch calls to the object. The synchronization attribute is largely independent of the ThreadingModel attribute, although some combinations of the two are incompatible. For example, components that have ThreadingModel="Apartment" require either Synchronization="Required" or Synchronization="Required New".
By setting ThreadingModel="Neutral" and Synchronization="Required", one can get a variation of a TNA that allows any thread to call into the object, but only one thread can call at any given time. Such a configuration is often referred to as the rental-threading model.
To ensure that no two threads can enter an activity concurrently, COM+ allocates a process-wide lock for each activity. When a proxy tries to enter a context in an activity, the proxy s interceptor attempts to acquire ownership of the lock. If the lock is available, the call proceeds. If not, the caller waits until it can get ownership of the lock. Once the method call returns to the interceptor, the lock is released, potentially allowing the next caller to enter the activity.
While the above mechanism ensures that only one thread can enter an activity at a time, it doesn t prevent the possibility of a deadlock. Consider for example, two objects, A and B, that belong to two different activities on two different machines. Consider the case of nested method calls where the client calls a method on A, A calls a method on B, and B calls a method on A. When the client calls a method on A s proxy, the proxy s interceptor locks A s activity. Object A then proceeds to call a method on B. This is a blocking call waiting for the response from B. Now, B tries to enter A s activity. But A s activity is already locked. We now have a situation where the call from A to B is blocked and the call from B to A is blocked. A deadlock, indeed! This scenario can easily be extended to any number of objects making a chain of calls on the same call stack.
To prevent such deadlocks, COM defines the notion of causality, which you can think of as the logical ID of a stack of nested calls. Due to the synchronous nature of method invocations, causality has a single logical thread of control throughout the network, despite the fact that several physical threads may be used to service the calls.
Causality begins when a thread makes a method call on the proxy. COM generates a causality ID and tags it to this method call and to all the subsequent nested calls from object to object, even across host machines. An activity gets locked in the context of a causality ID. If an incoming call arrives while an activity is locked, COM checks if the causality ID of the incoming call matches that of the one that locked the activity. If the IDs match, COM lets the call be serviced. If the incoming call is from a different causality, COM correctly blocks its entrance to the activity. Thus, by allowing reentrancy from the same caller, COM solves the deadlock problem.
|< BACK  NEXT >|