Condividi tramite


Understanding and Using COM Threading Models

 

Jason Smith
Software Design Engineer
Microsoft Corporation

July 31, 1998

Introduction

In this article, I'll explain some basic principles of the various threading models available in COM so that you can better understand how to take advantage of each model's attributes. This article will help you understand:

  • How to create apartments of different types
  • How the apartments operate
  • How to create objects in specific apartments
  • The advantages and disadvantages of each model

At the end of this article, I include a list of books and other articles that I've found to be very helpful. For brevity, I skip over some of the details that they discuss so well. At a minimum, I recommend reading the Knowledge Base article Q150777.

Objects and Apartments

If you've so much as cracked a book on COM, you've undoubtedly come across the term apartment, which describes the constructs in which COM objects are created. If you're unfamiliar with this concept, the often-used analogy of an apartment building may help to clarify.

If you think of the building as an application's process, each apartment is a distinct area in which a COM object can be created. As with the more tangible kind of apartment, a COM apartment is a fairly self-contained unit that shares some similarities with other apartments in the same building/process. But just as there may be lofts, studios, and 2- or 3-bedroom apartments in the same building, there can also be different types of COM apartments within a process. Apartments may also be vacant or have any number of people/objects living in them.

It may be helpful to remember a few key points about COM apartments:

  • An apartment is not a thread. The one-to-one relationship between threads and single-threaded apartments may lead you to believe that the two terms are interchangeable—they are not.
  • An object is not an apartment. Objects are created in apartments.
  • A particular instance of an object can belong to only one apartment.
  • An apartment's concurrency model, whether it is single-threaded or multi-threaded, cannot be changed after it is created.
  • A process can have zero or more single-threaded apartments—one for each thread that calls CoInitialize.
  • A process has one multi-threaded apartment or none at all—all threads that call CoInitializeEx with COINIT_MULTITHREADED share the same apartment.

Behavior of the COM Threading Models

Before any thread can create or manipulate COM objects, it must perform some preliminary initialization to establish its relationship with the COM library. As a result of this process, COM creates an apartment appropriate for the initialization routine: CoInitialize creates a single-threaded apartment (STA), whereas CoInitializeEx with the COINIT_MULTITHREADED flag produces a multi-threaded apartment. The CoInitialize/CoInitializeEx call does not initialize the COM library globally, only the calling thread's use of it, so it's important to remember that this initialization should be done on a per-thread basis. This is typically done early in a thread's work function (ThreadProc).

A single-threaded apartment is associated with the thread that created it, and only that specific thread may execute calls on objects within the apartment. By contrast, a multi-threaded apartment is not associated with any particular thread. It may be called concurrently by any number of threads and objects within it and should subsequently protect their member data.

Communication between apartments is done via marshaling, a generic abstraction for passing data across thread or process boundaries. Because calls to single-threaded apartments can only be executed on the thread that created them, other threads that wish to use an object within this apartment marshal the call to the apartment's thread and let the apartment thread execute the call. The apartment thread then marshals the return value back to the calling thread.

Single-Threaded Apartments

A process has one single-threaded apartment (STA) for each thread that called CoInitialize. Each of these apartments, in turn, may have zero or more COM objects associated with them. As the name implies, however, only one specific thread (the thread that created the apartment by calling CoInitialize) may directly access the objects within the apartment. Once associated with an apartment, a thread cannot be associated with another apartment or change the concurrency model of its apartment.

To create an STA, a thread simply calls CoInitialize or CoInitializeEx(NULL, COINIT_APARTMENTTHREADED), thereby associating the calling thread with the apartment until it calls CoUninitialize. Successive calls to either of these functions by the same thread do not create additional apartments or enable you to change the concurrency model of the apartment.

STAs are the simplest COM threading model to develop for, because COM itself performs much of the work of synchronizing access by other threads to objects within an apartment.

Note Because multiple instances of an object may be created in different apartments, global data is not thread-safe in an STA and should be protected. Critical sections, which allow exclusive access to a segment of code to a single thread, are most often used for this purpose. A discussion of the different thread synchronization mechanisms is outside the scope of this article.

Rather than using thread synchronization objects (mutexes, semaphores, and so forth) to control access to an object by several threads, the marshaling process for STAs translates the call into a WM_USER message and posts the result to a hidden top-level window associated with the apartment when it was created. Calls from several different threads are converted and sent to wait in the apartment's message queue until they are retrieved, dispatched, and executed on the apartment's thread. This process controls access to an apartment's objects by forcing incoming calls to pass through a sort of turnstile, thereby rendering the object's interface implementations thread-safe. An additional benefit of using the message queue is that inter-apartment COM calls are synchronized with window messages, which provides a means for objects that interact with users to synchronize incoming COM calls with the operation of the user interface (UI).

If your object uses thread synchronization objects to communicate between threads, it is important to remember this synchronization method. If the apartment's thread is suspended without specifying that it wants to be reawakened for windows messages, such as with a call to WaitForObject, the apartment's message queue will not be pumped and access to the apartment will be blocked. At best, all other objects in the same apartment will appear to hang as they wait for the thread to reawaken and process the message queue. The result can be even worse if, for example, your object is waiting for an asynchronous moniker to finish binding, resulting in a deadlock. To ensure that your object does not inadvertently block itself or other objects in the apartment, always use MsgWaitForObject or MsgWaitForMultipleObjects when suspending thread execution.

// create an asynchronous URL Moniker
hr = CreateURLMoniker(NULL, bstrURL, &spMoniker);
hr = CreateBindCtx(0, &spBindCtx);

// CMyObj class implements IBindStatusCallback
hr = RegisterBindStatusCallback(spBindCtx,
 reinterpret_cast<IBindStatusCallback*>
 (static_cast<IBindStatusCallbackImpl<CMyObj>*>
 (this)), 0, 0L);

// create event to signal completion of download
hDoneLoading = ::CreateEvent(NULL, FALSE, FALSE, NULL);

// begin binding. Uses IBindStatusCallback to get
// binding parameters (asynchronous, etc.)
hr = spMoniker->BindToStorage(spBindCtx, 0,
 IID_IStream, (void**)&pStream);

// wait for hDoneLoading event
while (TRUE)
{
 // wait for the event and for messages
 DWORD dwReturn = ::MsgWaitForMultipleObjects(1,
  &m_hDoneLoading, FALSE, INFINITE, QS_ALLINPUT);

 // this thread has been reawakened. Determine why
 // and handle appropriately.
 if (dwReturn == WAIT_OBJECT_0)
  // our event happened.
  break ;
 else if (dwReturn == WAIT_OBJECT_0 + 1)
 {
  // handle windows messages to maintain
  // client liveness
  MSG msg ;
  while(::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
   ::DispatchMessage(&msg) ;
 }
}

The input mask flag for MsgWaitForObject can be changed to a smaller subset, but it must, at the very least, include QS_POSTMESSAGE to insure that COM messages are pumped and handled as needed.

For in-proc (that is, DLL-based) COM servers, two values for the ThreadingModel registry value result in a component being created in an STA. If a component does not specify a ThreadingModel, it is assumed that the component does not implement any form of thread synchronization and neither its global data or DLL entry points are thread-safe. This component is subsequently created in the main STA, the first thread in the process to call CoInitialize. This model should be avoided because it can be extremely slow (see Knowledge Base article Q150777 for further information).

To avoid this sluggishness, be sure to specify the STA registry value ThreadingModel=Apartment. This states that a component's global data and DLL entry points are thread-safe which, in turn, allows multiple instances of this component to be created in any number of STAs.

Multi-Threaded Apartments

Although multi-threaded apartments, sometimes called free-threaded apartments, are a much simpler model, they are more difficult to develop for because the developer must implement the thread synchronization for the objects, a decidedly nontrivial task. On the positive side, removing an STA's synchronization mechanism gives the developer much finer control over the use of thread synchronization. They can apply it where it is actually needed rather than taking the very conservative approach of STAs, which synchronize access to the entire apartment.

The first thread in a process to call CoInitializeEx(NULL, COINIT_MULTITHREADED) creates a multi-threaded apartment that will be shared with any other threads that repeat this call. Once a thread is associated with the MTA, it can directly access any object created therein, which introduces some subtle variations from the way STAs operate. In STAs, a specific thread, permanently associated with the apartment, creates an object and only this thread can ever access the object (as discussed in the preceding section). In an MTA, however, many threads may create objects in the apartment and no thread controls access. As a result, it is possible to have several threads simultaneously access an object and change class data; hence the need for synchronization objects to protect class data.

It should come as no surprise that, as with STAs, global data is not thread-safe in an MTA and always needs to be adequately protected. Remember to take advantage of the inherently thread-local nature of local and automatic variables: they're allocated on the thread's stack and consequently do not need to be protected.

In-process components written for MTA use the ThreadingModel registry value (ThreadingModel = Free) to indicate to clients that they should be created in the MTA. An in-process component marked as such will be created in the MTA, regardless of the apartment type of the client. If necessary, COM will create the MTA before creating the component.

Mixed Model Development

The term "mixed model" sounds a little strange, but using COM's mixed model provides some impressive performance enhancements. As I briefly touched upon earlier, objects in different apartments cannot, as a general rule, directly access one another and are forced to marshal any communication between them. Objects that aggregate the free-threaded marshaler are an exception and can provide direct access to clients in other apartments within the same process. I won't discuss its use here, but provide a few excellent references for you to investigate at the end of this article.

If an STA client were to create a free-threaded component, COM would check the ThreadingModel registry value, see that the component was marked as "Free," and create it in the process' MTA. The resultant inter-apartment marshaling might degrade performance enough to negate all the work put into making an efficient, free-threaded component. Fortunately, the mixed threading model gives your component some convenient chameleon-like abilities so that it can be created in the same apartment as its client. If an STA client were to create a ThreadingModel=Both component, it would do so in the client's apartment. Likewise, if an MTA client created another instance of the same component, the second instance would be created in the MTA. The following table may help to understand the various permutations a little better (see the Summary of Client and Object Threading Models at the end of Knowledge Base Article Q150777 for additional details).

Client Model Server Model Server created in: Inter-object communication via:
STA MTA MTA (created if one doesn't already exist) Marshaling, unless server implements the free-threaded marshaler and is in the same process
MTA STA A new STA, created by COM Marshaling
STA Both Client's STA Direct access
MTA Both MTA Direct access

Obviously, this indeterminate threading model requires that you prepare the object for use in either type of apartment. Thread synchronization objects will still be needed to ensure that the component behaves reliably in an MTA. In an STA, they will be superfluous and mostly harmless, degrading performance only slightly because of the unnecessary locking of the synchronization objects.

The threading model registry entry for mixed model components is ThreadingModel=Both. Don't think that an object is shared between or straddles different types of apartments. Each instance of the component is created in and belongs to a single apartment, be it an MTA or STA.

Wrappin' It Up

You should now have a fair understanding of how to write COM objects that can use (or can themselves be used by) multiple threads, either to access several different objects on dedicated threads, as with multiple STAs, or to access a single object with multiple threads as with an MTA. If you find yourself longing for some implementation details, take a gander at the source code for ATL's CComAutoThreadModule class. This class creates a pool of threads and associates each one with a new STA. As clients create objects in the module, they are distributed throughout the thread pool.

For More Information

Agility in Server Components by Neil Allain

Q150777: Descriptions and Workings of OLE Threading Models, Microsoft Knowledge Base

Developing Active Server Components with ATL by George V. Reilly

For information on using the free-threaded marshaler:

Inside COM by Dale Rogerson