Over the past decade, Windows has evolved from a cooperatively multitasked 16-bit environment into a preemptively multitasked 32-bit operating system. In the process, threads have slowly but surely found their way into nearly all aspects of the operating system, from the C run-time library to the Microsoft Foundation Class (MFC) library. This transition has at times been painful, but the advantages have far outweighed the temporary inconveniences caused by the integration of threading. Because COM entered the threading game rather late, developers have experienced more than a little consternation as threading support has been added.
Around the time that multithreading support was added to COM, many of Microsoft's design decisions were affected by the amount of existing thread-unsafe legacy code.1 Microsoft felt that it was crucial to enable existing thread-unsafe components to interoperate seamlessly with the new multithreaded components. Thus, it defined several levels, called threading models, of thread safety. The basic unit of thread safety in COM+ is called an apartment. An apartment is a set of threading rules shared by a group of objects. As such, an apartment cannot be compared with a Win32 object such as a process or a thread. Instead, the apartment concept serves only to clarify the threading rules applied to a COM+ object; these rules differ by apartment type.
The three types of apartments are single-threaded apartments (STAs), neutral apartments (NAs), and multithreaded apartments (MTAs). STAs can have only one thread executing per apartment, while NAs and MTAs can have multiple threads executing per apartment. Method calls to objects in an STA are automatically synchronized and dispatched using window message queues, while method calls to objects in an MTA or NA are not. Not all platforms that support COM+ support all apartment types. The Windows CE operating system, for instance, currently supports only the MTA model.
Immediately to the right of Java Man on the evolutionary time line stands OLE 1, a compound document architecture that originally used Dynamic Data Exchange (DDE) messages for its interprocess communication. Until the advent of the MTA model, COM had not entirely shed its seedy, message-based past. In fact, to this day the STA model notifies an object of method calls by posting messages to a window message queue. That's right: any component2 that supports the STA model must contain a message loop such as the one shown below or nothing will happen:
// Bet you thought you'd never see another one of these! MSG msg; while(GetMessage(&msg, 0, 0, 0)) DispatchMessage(&msg); |
Why, you ask in disbelief, does a component architecture with as much finesse as COM+ rely on window message queues? By relying on message queues to dispense method calls, COM+ effectively has hooks into every object in an STA. Since messages are processed sequentially, method calls to objects in an STA are automatically synchronized to ensure that thread safety is not compromised. The MTA model does not rely on message queues because COM+ does not have to provide thread synchronization for such objects.
In the past, the combinations of STAs and MTAs in a single process have been dubbed single-threading, apartment-threading, free-threading, and mixed-threading models. You should avoid this terminology because it simply causes additional confusion, even though some documentation might still refer to COM+ threading models using these obsolete names. A good way to think of these threading models is to remember that they all rely on the basic unit of an apartment. Some of those apartments can have only a single thread (STAs); others support multiple threads (MTAs and NAs). Note that all three apartment types can be combined in a single process; a process can contain zero or more STAs but at most one MTA and one NA.
The threading models in COM+ allow clients and components that use different threading architectures to work together. From the perspective of a client, all objects appear to use the client's threading model. Likewise, from the perspective of an object, all clients appear to support the object's threading model. Clients and components have different responsibilities in relation to the threading models. Clients can use the Win32 CreateThread function to launch new threads of execution and can access COM+ objects from these new threads. This technique is often used to improve the responsiveness of an application or to enable the application to access several different objects concurrently. Components, on the other hand, rarely call the CreateThread function, and doing so is strongly discouraged. Instead, a component simply declares its threading model at start-up and then lets COM+ handle the concurrency requirements mandated by the particular situation.
The current STA model is descended from the original thread-oblivious model used by OLE. In the latter model, objects could be accessed only from a single thread in the process. The modern STA model evolved to overcome this limitation; it allows a single process to contain multiple STAs. Since each STA has one and only one thread associated with it, the ability of multiple STAs to exist in a single process means that several threads can use different COM+ objects concurrently—a big improvement. Legacy components written prior to the advent of the COM+ threading models actually run in a single STA, called the main STA.
A particular thread of execution declares its support for a threading model by calling CoInitializeEx. The second parameter of CoInitializeEx specifies whether the thread supports the STA (COINIT_APARTMENTTHREADED) model or the MTA (COINIT_MULTITHREADED) model. The obsolete function, CoInitialize, calls the CoInitializeEx function with the COINIT_ APARTMENTTHREADED flag, as shown in the following code fragment. In part because these two calls are equivalent, legacy components are considered to support the main STA model.
// Single-threaded apartment (STA) // This code is equivalent to CoInitialize(NULL). CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); |
The STA model owns any COM+ objects instantiated by its thread, so all method calls on the object are executed by the thread that created the object. The fact that the same thread that created an object is always used to execute its methods is important to objects that have thread affinity; an object that requires thread affinity must be executed by a particular thread. For example, some objects use thread local storage (TLS) to associate data with a specific thread. An object using TLS expects that the same thread will be used to execute all of its methods. If a different thread executes a method, any attempt to access TLS data will fail. Because objects running in the MTA or NA model can be executed on a variety of different threads, they cannot use TLS. Only objects running in an STA may have thread affinity.
As in the original thread-oblivious model, method calls on objects running in an STA are dispatched using window message queues. This does not mean that a component must display a user interface or even create a window. The first call to CoInitializeEx with the COINIT_APARTMENTTHREADED flag in a process calls the Win32 function RegisterClass to register the OleMainThreadWndClass window class. During this and every subsequent call to CoInitializeEx with the COINIT_APARTMENTTHREADED flag, the system automatically calls the Win32 function CreateWindowEx to create a hidden window of the OleMainThreadWndClass window class for each STA. The message queue of this hidden window is used to synchronize and dispatch COM+ method calls on this object. For this reason, the thread associated with an STA must retrieve and dispatch window messages using GetMessage and DispatchMessage or similar functions.
You can display the hidden window using a utility such as Microsoft Spy++, which comes with Microsoft Visual C++, as shown in Figure 4-1. A method call is received as a message to this hidden window. When the component retrieves the message (GetMessage) and then dispatches it (DispatchMessage), the hidden window's window procedure (wndproc) receives the message. This window procedure, implemented as part of COM+, then calls the corresponding interface method of the object.
Figure 4-1. The hidden STA window created by COM+ and uncovered using Microsoft Spy++.
This message-queuing architecture solves the problem of multiple clients making concurrent calls to an object running in an STA. Since all the calls are submitted as window messages posted to a message queue, the calls are serialized automatically. The object receives method calls in an orderly fashion when the message loop retrieves and dispatches the messages in the queue. Because COM+ serializes the calls in this manner, the object's methods do not need to provide synchronization. Note that the dispatched method calls are always made on the thread that instantiated the object. You can easily verify this using the Win32 GetCurrentThreadId function or the CoGetCurrentProcess3 function to retrieve the thread identifier in each method of an object. The thread identifier is always the same for all objects running in one STA.
In some circumstances, an object can be reentered by the same thread, similar to the way in which a window procedure can be reentered. If a method of the object processes window messages, another call to a method might be dispatched. You can easily avoid this problem by not processing window messages during a method call. An object might also be reentered if a method makes an outgoing call and the outgoing method then calls back into the object, perhaps via a mechanism such as connection points. (For more on connection points, see Chapter 8.) COM+ does not prevent these types of reentrancy; it guarantees only that the calls will not execute concurrently.
An STA's message-based method invocation mechanism is used even when the client and component are separated by a network. Figure 4-2 shows how the STA model works. Each number indicates a separate thread of execution. In the first thread, the client calls the proxy, which in turn calls the channel. (The COM+ channel is basically a wrapper around the RPC marshaling infrastructure.) Then a second thread is created to block while sending the data across the network using an RPC. Meanwhile, the first thread sits in a message loop waiting for incoming calls and other window messages. This message loop operates until the RPC call made by the second thread notifies it that a response has been received.
Figure 4-2. The inner workings of the STA model.
Across the network, a server-side thread takes the received data packet and posts a message containing the client request to the message queue of the hidden window. Another message loop is then needed to take messages from the queue and dispatch them back to the channel's internal window procedure, which in turn calls the stub using the object's thread. Finally, the stub unpacks the parameters and calls the desired method in the object. The process of packing parameters for transmission is called marshaling and is basically repeated in reverse order once the method has finished executing. (For more on marshaling and the IRpcProxyBuffer, IRpcChannelBuffer, and IRpcStubBuffer interfaces, see Chapter 15.)
Note that four threads—two on the client side and two on the component side—are required to make the STA model work. An internal optimization of the STA model is possible when the client and the component both execute on the same machine. In such cases, COM+ automatically uses only two threads—one in the client process and one in the component. In this case, the reentrancy requirements of the STA model are met using a system supplied callback function to the RPC transport that retrieves and dispatches window messages.
An executable component can implement the IMessageFilter interface in each STA to control aspects of the call delivery mechanism employed by COM+. Building a custom message filter allows an application that uses STAs to better integrate COM+ calls with UI-driven threads. You can use this technique to abort a lengthy method call if the user gets tired of waiting. Note that since message filters work only with the STA model, they do not offer a generic way to cancel method invocations. COM+ also supports a generic call cancellation facility for all apartment types; see Chapter 17 for details.
An application that supplies a custom message filter must provide COM+ with a pointer to its implementation of the IMessageFilter interface via the CoRegisterMessageFilter function, as shown here:
IMessageFilter* pMF = new CMessageFilter; IMessageFilter* pOldMF; CoRegisterMessageFilter(pMF, &pOldMF); |
CoRegisterMessageFilter installs the new message filter and returns a pointer to the previous message filter, enabling the caller to restore the previous message filter (if any) at a later time. (Since COM+ provides a default message filter for STAs, it is unlikely that the pointer to the previous message filter will be NULL unless you have purposely passed a null pointer to the CoRegisterMessageFilter function.) The IDL definition of the IMessageFilter interface is shown below.
interface IMessageFilter : IUnknown { // Called in a component to notify it of an incoming // method call DWORD HandleInComingCall( [in] DWORD dwCallType, // Type of incoming call [in] HTASK htaskCaller, // HTASK of the calling task [in] DWORD dwTickCount, // Elapsed time since call // was made [in] LPINTERFACEINFO lpInterfaceInfo); // More info // (see below) // Called in a client to notify it that the component has // rejected or postponed a call DWORD RetryRejectedCall( [in] HTASK htaskCallee, // Server task handle [in] DWORD dwTickCount, // Elapsed tick count [in] DWORD dwRejectType); // Returned rejected message // Called in a client when a window message is received // while a method call is pending DWORD MessagePending( [in] HTASK htaskCallee, // Called application's // task handle [in] DWORD dwTickCount, // Elapsed tick count [in] DWORD dwPendingType); // Call type } |
An object's implementation of the IMessageFilter::HandleInComingCall method is called by COM+ when an incoming method invocation is received, giving the apartment the opportunity to accept, reject, or postpone the call. The first parameter of the HandleInComingCall method specifies the nature of the call made by the client; it can be one of the following values:
// Call types used by IMessageFilter::HandleInComingCall typedef enum tagCALLTYPE { CALLTYPE_TOPLEVEL = 1, // Top-level call -- no outgoing call CALLTYPE_NESTED = 2, // Callback on behalf of previous // outgoing call -- should // always handle CALLTYPE_ASYNC = 3, // Async call -- cannot be rejected CALLTYPE_TOPLEVEL_CALLPENDING = 4, // New top-level call // with a new logical // thread ID CALLTYPE_ASYNC_CALLPENDING = 5 // Async call -- cannot // be rejected } CALLTYPE; |
The CALLTYPE_NESTED flag indicates that a reentrant call is being made while the apartment is involved in executing some other method on behalf of the same client; the CALLTYPE_TOPLEVEL flag indicates that no other active calls are pending in the apartment. The fourth parameter of the IMessageFilter:: HandleInComingCall method provides a pointer to an INTERFACEINFO structure, shown below. The INTERFACEINFO structure tells the message filter which object is receiving a call, the IID of the interface implemented by that object, and the method number in the interface.
// Additional interface information about the incoming call typedef struct tagINTERFACEINFO { IUnknown *pUnk; // The pointer to the object IID iid; // Interface id WORD wMethod; // Interface method } INTERFACEINFO, *LPINTERFACEINFO; |
Based on the information provided, the message filter's implementation of the IMessageFilter::HandleInComingCall method can return one of the three values shown in the enumeration shown below. Basically, the method should return SERVERCALL_ISHANDLED if it wants to accept the call, SERVERCALL_REJECTED if it cannot process the request, or SERVERCALL_RETRYLATER if the object cannot handle the request currently but might be able to later.
// Values returned by IMessageFilter::HandleInComingCall and // passed to the IMessageFilter::RetryRejectedCall method on // the client typedef enum tagSERVERCALL { SERVERCALL_ISHANDLED = 0, // Process the call SERVERCALL_REJECTED = 1, // Reject the call SERVERCALL_RETRYLATER = 2 // Tell the caller to try // back later } SERVERCALL; |
If a message filter rejects or postpones a call by returning the value SERVERCALL_REJECTED or SERVERCALL_RETRYLATER from the IMessageFilter::HandleInComingCall method, the system immediately calls the IMessageFilter::RetryRejectedCall method on the client's implementation of the IMessageFilter interface. The third parameter of the RetryRejectedCall method indicates the value returned by the object's message filter from the HandleInComingCall method; this is either SERVERCALL_REJECTED or SERVERCALL_RETRYLATER.
Typically, a client application silently retries calls that have failed with the SERVERCALL_RETRYLATER flag. If, after a reasonable period of time, the call still does not go through, it is reasonable to inform the user and cancel the request. The COM+ channel uses the value returned from the IMessageFilter::RetryRejectedCall method to determine what action to take. A value of _1 indicates that the call should be canceled, causing the proxy to return the value RPC_E_CALL_REJECTED from the canceled method. This is the action that the default COM+ message filter takes. If the RetryRejectedCall method returns a value greater than _1 but smaller than 100, COM+ retries the call immediately. A value equal to or greater than 100 causes the system to wait the specified number of milliseconds and then retry the call.
The system calls the IMessageFilter::MessagePending method in client applications if a window message appears in the application's message queue while an outstanding method call is pending. The default message filter allows certain window messages (such as WM_PAINT) to be dispatched while a call is pending. This avoids freezing the UI and giving the impression that the application has crashed. Input messages, such as messages originating from the mouse and keyboard, are generally discarded so the user does not begin another operation while the call is pending. The third parameter of the MessagePending method can specify one of the following values to indicate whether the pending call is a top-level or nested call.
// Pending type indicates the level of nesting typedef enum tagPENDINGTYPE { PENDINGTYPE_TOPLEVEL = 1, // Top-level call PENDINGTYPE_NESTED = 2 // Nested call } PENDINGTYPE; |
The IMessageFilter::MessagePending method should return one of the values shown in the following PENDINGMSG enumeration. Returning the value PENDINGMSG_CANCELCALL cancels the outstanding method call, causing the outstanding method call to fail with the value RPC_E_CALL_CANCELLED. PENDINGMSG_WAITDEFPROCESS defers processing the message until the outstanding method call completes. PENDINGMSG_WAITNOPROCESS discards the message and waits for the pending method to return. Note that the client apartment's message filter can display the standard busy dialog box to the user by calling the OleUIBusy function. A sample implementation of the IMessageFilter interface is available on the companion CD in the Samples\Apartments\Message Filters folder.
// Return values of MessagePending typedef enum tagPENDINGMSG { PENDINGMSG_CANCELCALL = 0, // Cancel the outgoing call PENDINGMSG_WAITNOPROCESS = 1, // Wait for the return and // don't dispatch the message PENDINGMSG_WAITDEFPROCESS = 2 // Wait and dispatch the message } PENDINGMSG; |
A thread enters the MTA model by calling CoInitializeEx(NULL, COINIT_MULTITHREADED), as shown in the code fragment below. Because only one MTA is ever created in a process, only the first thread to call the CoInitializeEx function using the COINIT_MULTITHREADED flag creates the MTA. Every subsequent thread that calls the CoInitializeEx function using this flag joins the existing MTA.4
// Enter the MTA. CoInitializeEx(NULL, COINIT_MULTITHREADED); |
Threads running in the MTA do not need to retrieve or dispatch window messages because COM+ does not use messages to deliver method calls on objects running in the MTA. Since method calls are made directly through a v-table, COM+ does not impose any synchronization on objects running in the MTA. Therefore, these objects must provide their own synchronization using critical sections, events, mutexes, semaphores, or other mechanisms as dictated by their synchronization requirements.
Multiple clients can concurrently execute an object's methods from different threads, and thus threads of an MTA cannot use TLS or have any other thread affinity whatsoever. An object running in the MTA receives calls through a pool of COM+-allocated threads5 belonging to the object's process. The system creates these threads at run time and reuses them as appropriate. Since the system automatically spawns threads as necessary to enable concurrent access to the component, you should avoid calling the Win32 CreateThread function. In fact, calling CreateThread to enable concurrency in a component is strongly discouraged because in most cases this will interfere with the system's threading pooling algorithm. A utility such as Process Viewer (pview.exe), included with Visual C++, can be useful for dynamically spying on the threads created by COM+.
As you know, client applications bind threads to apartments by calling the CoInitializeEx function. Any objects instantiated by those threads become members of the apartment to which the thread belongs. The client can call the CreateThread function to spawn additional threads, and each thread that wants to access COM+ objects must declare its apartment type by calling CoInitializeEx. The following code fragment shows how a thread in the client process creates an STA and instantiates a coclass:
void __stdcall ThreadRoutine(void) { CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); IUnknown* pUnknown; HRESULT hr = CoCreateInstance(CLSID_InsideCOM, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pUnknown); pUnknown->Release(); CoUninitialize(); } |
The IUnknown pointer returned by the CoCreateInstance call in the preceding code can be used only in this apartment. For example, if this pointer were passed through a global variable to the thread of another apartment and then used to call a method, an RPC_E_WRONG_THREAD error could result. This error is returned by the interface proxy if the client attempts to use an interface pointer that was not marshaled for use in a different apartment.
Remember that interface pointers in COM+ are apartment-relative—that is, they cannot be used by a thread in another apartment unless special precautions are taken. If COM+ were to allow the sharing of raw interface pointers, it would have no way of guaranteeing the synchronization required by the different apartment models because the object could be called directly from other apartments on other threads. Instead, the system provides the CoMarshalInterThreadInterfaceInStream and CoGetInterfaceAndReleaseStream functions to enable the marshaling of an interface pointer from one apartment to another. CoMarshalInterThreadInterfaceInStream marshals the apartment-neutral representation of an interface pointer into a stream object; its declaration is as follows.
WINOLEAPI CoMarshalInterThreadInterfaceInStream(IN REFIID riid, IN LPUNKNOWN pUnk, OUT LPSTREAM *ppStm); |
Let's say that the thread of an STA—let's call it STA1—wants to pass an interface pointer to the thread of another STA, which we'll call STA2. STA1 first calls CoMarshalInterThreadInterfaceInStream to obtain an apartment-neutral interface representation stored in a stream object. The first parameter of CoMarshalInterThreadInterfaceInStream is the IID of the interface that is being marshaled, and the second parameter is a pointer to the object that implements that interface. The resultant IStream pointer returned in the third parameter can then be stored in a global variable accessible to STA2. STA2 passes this IStream pointer to the CoGetInterfaceAndReleaseStream function to unmarshal the stream and obtain an apartment-relative interface pointer to a proxy that is appropriate for use in STA2. Any calls made by the STA2 thread to the object in STA1 are now made through the proxy that was loaded in STA2 when the interface pointer was unmarshaled, as illustrated in Figure 4-3. The declaration of the CoGetInterfaceAndReleaseStream function is shown below:
WINOLEAPI CoGetInterfaceAndReleaseStream(IN LPSTREAM pStm, IN REFIID iid, OUT LPVOID FAR* ppv); |
Figure 4-3. The thread of one STA passing an interface pointer to another STA.
As shown in the following client-side code, the thread in STA1 instantiates a COM+ object and then calls CoMarshalInterThreadInterfaceInStream (shown in boldface) to marshal that interface pointer into an apartment-neutral representation. The resultant IStream pointer is then passed as an argument to a newly spawned thread. STA1 can now continue to use its interface pointer, and when it finishes working, it can release the pointer without danger. Notice that the IStream pointer is not released in this code; it is released by the receiving thread.
// STA1 IMyInterface* pMyInterface; hr = CoCreateInstance(CLSID_MyCOMClass, NULL, CLSCTX_LOCAL_SERVER, IID_IMyInterface, (void**)&pMyInterface); // Marshal interface pointer to stream. IStream* pStream; hr = CoMarshalInterThreadInterfaceInStream(IID_IMyInterface, pMyInterface, &pStream); // Spawn new thread; // pass pStream to new thread. DWORD threadId; CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadRoutine, pStream, 0, &threadId); // Do other work... pMyInterface->Release(); |
The new thread begins execution at the ThreadRoutine function. Its only argument is the IStream pointer containing the marshaled interface pointer to the object it wants to call, as shown in the following code. This code first creates a new STA, STA2, by calling CoInitializeEx, and then it unmarshals the stream object by calling CoGetInterfaceAndReleaseStream (shown in boldface). This has the effect of returning an apartment-relative interface pointer usable by STA2 and automatically releasing the stream. When it finishes using the interface pointer, STA2 must release it. Any method calls invoked on the interface pointer returned by CoGetInterfaceAndReleaseStream are marshaled back to the actual object in STA1 by the proxy loaded in STA2.
void __stdcall ThreadRoutine(IStream* pStream) { // Create STA2. hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); // Unmarshal interface pointer from stream. IMyInterface* pMyInterface; hr = CoGetInterfaceAndReleaseStream(pStream, IID_IMyInterface, (void**)&pMyInterface); // Do work with pMyInterface... pMyInterface->Release(); CoUninitialize(); } |
Their long and sophisticated-sounding names notwithstanding, CoMarshalInterThreadInterfaceInStream and CoGetInterfaceAndReleaseStream are basically wrapper functions for CoMarshalInterface and CoUnmarshalInterface. Interestingly, the CoMarshalInterThreadInterfaceInStream function calls CoMarshalInterface with the MSHCTX_INPROC flag. This flag was added to CoMarshalInterface6 to indicate that the unmarshaling will be performed in another apartment of the same process. The following pseudocode shows the internals of CoMarshalInterThreadInterfaceInStream and CoGetInterfaceAndReleaseStream:
HRESULT MyMarshalInterThreadInterfaceInStream(REFIID riid, IUnknown* pUnknown, IStream** pStream) { CreateStreamOnHGlobal(NULL, TRUE, pStream); return CoMarshalInterface(*pStream, riid, pUnknown, MSHCTX_INPROC, NULL, MSHLFLAGS_NORMAL); } HRESULT MyGetInterfaceAndReleaseStream(IStream* pStream, REFIID riid, void** ppv) { HRESULT hr = CoUnmarshalInterface(pStream, riid, ppv); pStream->Release(); return hr; } |
As shown in Figure 4-4, interface pointers can be passed directly between threads running in the MTA; they do not need to be marshaled using the CoMarshalInterThreadInterfaceInStream and CoGetInterfaceAndReleaseStream functions. Of course, any attempt to pass an interface pointer from an MTA to an STA thread must be marshaled using the CoMarshalInterThreadInterfaceInStream and CoGetInterfaceAndReleaseStream functions. In addition, message filters implementing the IMessageFilter interface cannot be used in the MTA model.
Figure 4-4. Interface pointers passed directly between threads in an MTA.
Which threading model is right for you? Generally speaking, threads that interact directly with the user interface should use the STA model, while components that run without a significant user interface usually do best with the MTA model. Remember that in the STA model, window message queues are used to invoke methods in COM+ objects. Since any application that displays a window already has a message loop, the STA model is a natural fit. You can also use a combination of STAs, an NA, and an MTA in a single process when you need various threading models for different purposes in the same application. The following table compares the features of STAs, MTAs, and NAs.
If you are designing a new component without a significant user interface, the MTA model might seem like the obvious choice. This model is faster because the system does not need to synchronize calls into free-threaded objects. When you use the MTA model, however, the object must implement its own synchronization or fall prey to the thread synchronization problems discussed earlier.
Feature | STA | MTA | NA |
---|---|---|---|
Synchronization provided by COM+ | Yes | No | No |
Can have multiple threads executing in one apartment | No | Yes | Yes |
Must marshal interface pointers between threads of different apartments | Yes | Yes | Yes |
Must marshal interface pointers between threads in the same apartment | Not applicable; an STA has only one thread. | No | Not applicable; the NA has no resident threads. |
Uses window message queues | Yes | No | Depends. Yes if an STA thread is executing in the NA; no if it is executing on an MTA thread. |
Must call CoInitializeEx in every thread that uses COM+ services | Yes | Yes | Not applicable; there are only STA threads and MTA threads. |
In-process calls are always invoked on the caller's thread | No | No (unless the Yes object aggregates the Free-Threaded Marshaler). | Yes |
Can use TLS | Yes | No | No |
If you don't feel like dealing with thread synchronization issues, you can have the system do the work for you by selecting the STA model. In this model, COM+ synchronizes access to a component at the method level—in other words, it ensures that two different threads cannot call into an STA concurrently. You can simulate this behavior in an MTA by using a Win32 critical section, as shown in the following code. However, instead of building locks around entire methods as the STA does, a component that supports the MTA model normally uses locks only around small portions of the critical code within a method, resulting in improved concurrency.
class CMyClass { public: CMyClass::CMyClass() { // Create the critical section. InitializeCriticalSection(&m_cs); } CMyClass::~CMyClass() { // Before exiting, free the critical section. DeleteCriticalSection(&m_cs); } HRESULT __stdcall CMyClass::MyMethod(int MyParameter) { // Verify that no other method in the object is // being called. EnterCriticalSection(&m_cs); // Write your code here... // Leave the critical section, enabling another method // to execute. LeaveCriticalSection(&m_cs); return S_OK; } HRESULT __stdcall CMyClass::MyOtherMethod(int MyParameter) { EnterCriticalSection(&m_cs); // Write your code here... LeaveCriticalSection(&m_cs); return S_OK; } private: // The critical section data structure CRITICAL_SECTION m_cs; }; |