Threads and the COM Developer

 < Free Open Study > 



As a COM developer, you need to accept the fact that your object may be loaded into a process which has numerous threads running around, many of which might be coming to attack an instance of your coclass. For example, what if the Threads program was not accessing a FOOFIGHTER structure, but the IDraw interface of CoHexagon?

If you do not take the steps to provide your COM object with the correct armor, your data may be mangled, as numerous threads are attempting to change the state of your object at more or less the same time. To protect your COM objects from the violent world of multithreaded clients, you too must protect your data using any number of threading primitives such as critical sections, semaphores, or mutexes.

You should also be prepared for a single-threaded process to create and use your COM objects. In this case, all is well, and you may rest assured that a single thread will access your object at any given time. Under this condition, you do not have to worry about writing enormous amounts of code to keep your data safe (you should still ensure shared global and static data is thread safe).

Nevertheless, you cannot control which type of client (single-threaded or multi- threaded) will load your server and access the COM objects it contains. You can, however, advertise how you wish your objects to be handled under diverse threading conditions.

On a related note, a COM client should not need to worry if your object is thread safe. In other words, if a process containing ten threads wishes to create and use an instance of CoHexagon, the client assumes the object is ready to fend for itself. It would be a bother indeed if a COM client was only able to work with objects supporting the correct threading model. The Component Object Model would fail miserably if multithreaded clients could only use COM objects explicitly coded to work with multithreaded clients. It would be an additional bother if COM developers needed to create thread-safe and thread-oblivious versions of the same object to account for the possible threading option of the client. In order to account for the fact that multithreaded or single-threaded clients may load an object that may or may not be thread safe, COM provides a level of abstraction called an apartment.

Understanding COM Apartments

To understand what a COM apartment is, let's begin by learning what it is not. An apartment is not a process. An apartment is not a thread. An apartment is a conceptual entity which defines an execution context within a process. Apartments provide an environment for COM objects to live, based on their own level of thread awareness. When identifying the apartment model of a COM object, we are able to instruct the COM subsystem how we wish our objects to be handled under various threading conditions. Thus, it is possible to allow objects and clients to mingle together, regardless of each entity's level of thread awareness. COM apartments currently come in two varieties: single threaded (STA) or multithreaded (MTA).

The Single-Threaded Apartment (STA)

A single-threaded apartment (STA) contains exactly one thread (not too much of a stretch there, huh?). If your coclass supports this threading model, you instruct COM to ensure that only a single thread may access it at any given time. The end result of this model is that the developer of this object (that would be you) will not need to synchronize access to your object's state data, as COM ensures that thread access takes place using a queue. This is accomplished by the automatic creation of an invisible window used to serialize client access into the object through an associated message pump.

Note 

Objects living in an STA should still protect any global or static data from concurrent access using a Win32 locking primitive (CRITICAL_SECTION, et.al.).

Each STA in a process has a hidden window message pump controlling synchronized access to the COM objects it contains (this invisible window is based off the OleMainThreadWndClass window class). Therefore, if requests are coming to your object from numerous threads, they must form a line at the queue and wait for the former thread to finish up its business. As you can tell, the STA is a very safe place for a COM object to live. However, if multiple threads attempt to use your object, access can be less responsive. You may visualize the STA as the following:

In Figure 7-6, we can see that COM objects configured to live in an STA are protected from multiple clients (threads). The hidden window and the associated message pump force each thread to wait in line, as all requests are sent through the message pump in the apartment. For example, thread C is second in the queue, and cannot have its Draw() request honored until client A is finished with its Draw() request. Likewise, thread B is dead last in line, and must wait for C and A to finish up before it gets a chance to ask for CoDot to be rendered.

click to expand
Figure 7-6: Multiple threads must wait in line to access objects in an STA.

So, the STA is a safe place for COM objects to live, as the majority of the synchronization effort is provided automatically by the COM subsystem. However, you can suffer a performance hit if multiple clients are attempting to access an object. To allow faster access by multiple threads COM provides the multithreaded apartment (MTA).

The Multithreaded Apartment (MTA)

The multithreaded apartment (MTA) can contain any number of threads. Rather than providing serialized access via a background message pump, the MTA allows individual client requests to execute on a separate thread. COM objects supporting this model must be programmed to be accessed by multiple threads in a safe manner, thus the developer must write thread savvy code to ensure the state data in the object is protected from a possible bombardment of simultaneous requests. The MTA may be visualized as so:

click to expand
Figure 7-7: Objects in the MTA can be accessed by multiple threads at once.

In Figure 7-7, we have three threads accessing two COM objects loaded into the MTA. As the objects are residing in the MTA, they make the bold assertion that they are ready to take any number of client requests from any number of threads, and have been programmed accordingly. Threads A and B are both coming to ask CoHexagon to perform some action. If CoHexagon has its internal data protected by a critical section (or similar threading primitive), we have a thread-safe coclass. If not, we have data corruption.

Entering an Apartment

Understand that a single process may contain numerous apartments in various combinations. To be specific, a process may contain the following types of (and number of) apartments:

  • A process may contain exactly one MTA or no MTA at all.

  • A process may have zero or more STAs in addition to a single MTA.

The very first STA created in a process is called the main STA. Legacy COM objects that do not advertise a threading model (as there were no threading issues to contend with at the time) are loaded into the main STA automatically. An object in the main STA is completely safe from any threading issues, as COM ensures a single thread may access all state, global, and static data at a given time.

A logical question at this point is "How are these apartments created?" COM library functions, of course! As you know, when a client is preparing to use a coclass, the client must first call CoInitialize() before any further work can be done. This method is a legacy function of the COM library that initializes the COM libraries, and automatically specifies that the launching thread is about to join a new STA. Under the hood, CoInitialize() actually maps to a call to CoInitializeEx(). CoInitializeEx() allows a calling thread to specify which type of apartment it wishes to join via the dwCoInit parameter (the first parameter is reserved, and must be NULL):

// CoInitialize() is really mapped to CoInitializeEx(). // CoInitializeEx() allows a thread to join an STA or MTA. HRESULT CoInitializeEx(LPVOID pvReserved, DWORD dwCoInit);
Note 

Beware! You cannot use this COM library function until you define the _WIN32_DCOM symbol in your current project.

Assume a COM client wishes to use CoHexagon. Before doing so, the client must specify which apartment the calling thread is about to join. As you recall, a process always has a single main thread; thus, if your main thread wants to join the single process-wide MTA, you can write the following:

// Send the main thread into the process wide MTA. #define _WIN32_DCOM     // A must have! #include <windows.h> int main(int argc, char* argv[]) {      CoInitializeEx(NULL, COINIT_MULTITHREADED);      // Do some COM stuff...      CoUninitialize();      return 0; }

If the client would rather enter an STA, you may simply call CoInitialize(), or alternatively use CoInitializeEx() specifying COINIT_APARTMENTTHREADED:

// Sent the main thread into an STA. #define _WIN32_DCOM     // Still a must have! #include <windows.h> int main(int argc, char* argv[]) {      // This call to CoInitializeEx() is equivalent to CoInitialize(NULL).      CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);      // Do some other COM stuff...      CoUninitialize();      return 0; }

Specifying a Threading Model for EXE Servers

To make things more interesting, EXE COM servers also specify the apartment they wish to join by calling CoInitializeEx() (or the legacy CoInitialize()). When we developed our EXE COM server in Chapter 5, WinMain() began its life by joining a new STA:

// EXE servers get to specify the apartment they wish to join. int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,                    LPSTR lpCmdLine, int nCmdShow) {      // The objects in this server are not thread safe, so enter an STA.      CoInitialize(NULL);      // Register class objects...      // Start a message pump...      CoUninitialize();      return 0; }

If you have an EXE server that contains thread-safe coclasses, you may opt to enter the MTA:

// EXE servers get to specify the apartment they belong to. int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,                   LPSTR lpCmdLine, int nCmdShow) {      // Objects are thread safe, so let's join the MTA.      CoInitializeEx(NULL, COINIT_MULTITHREADED);      // Register class objects...      // Start a message pump...      CoUninitialize();      return 0; }

As you can see, every thread that is using COM must specify an apartment it wishes to join. This is true for both COM clients and EXE servers. If you are developing a COM client that spawns additional threads beyond the primary thread, they must all make a call to CoInitializeEx() before working with COM objects, and call CoUninitialize() when the worker thread has exited.

Specifying a Threading Model for DLL Servers

Now, we need to examine how coclasses living in DLLs get to advertise the type of apartment they wish to join. You may have noticed that the in-proc servers we have developed never called CoInitialize(), CoInitializeEx(), or CoUninitialize(). The reason is that in-proc servers always live in the context of the client, as the client has already called CoInitialize[Ex]().

To instruct COM how to handle the objects contained in an in-proc server, each coclass in the server is marked in the registry with the ThreadingModel value. This value is added to the InprocServer32 subkey of HKCR\CLSID\{guid}. For example, here is the ThreadingModel value of the ATL CoCar created in Chapter 6:

click to expand
Figure 7-8: In-process servers advertise their threading preference with the Threading- Model value.

If you go back and examine the RGS file for CoCar, you will see the syntax responsible for this entry:

ForceRemove {4DE3E4A3-16C6-11D3-B8F3-0020781238D4} = s 'CoCar Class' {      ProgID = s 'ATLCoCar.CoCar.1'      VersionIndependentProgID = s 'ATLCoCar.CoCar'      InprocServer32 = s '%MODULE%'      {           val ThreadingModel = s 'Apartment'      }      'TypeLib' = s '{4DE3E496-16C6-11D3-B8F3-0020781238D4}' }

The ThreadingModel value may take one of four settings, each of which is used to instruct COM how to treat your in-process objects with regard to matters of thread awareness:

  • (none): If there is an empty ThreadingModel value in the registry for an in-process coclass, COM assumes this object must join the very first STA created in a process. Recall, the first STA created in a process is called the main STA. An object in the main STA is 100% thread safe (including access to global and static data).

  • Apartment: Objects will load into some STA in the process. This ensures all instance data is thread safe, as every STA has serialized access provided by the hidden message pump. Your global and static data must still be made thread safe, as multiple objects of this type would share the same global data.

  • Free: Free threading is just another way to refer to the MTA. Objects marked as Free wish to be loaded into the process-wide MTA. These objects had better be 100% thread safe, as COM will not serialize thread requests through a hidden message pump. The job of creating thread-safe code is your responsibility.

  • Both: If the ApartmentModel value has been set to Both, your object is safe to work in either an STA or the MTA, based on the threading preferences of the client. Your objects must still be 100% thread safe.

Marshaling Revisited

If you recall the discussion of COM marshaling from Chapter 5, you remember that when a COM client wishes to access a COM object outside of its own process, it does so using stubs and proxies. It is through stubs and proxies that location transparency is achieved as both parties believe they are always communicating with an in-process entity. While this is technically correct, we did not address a very important aspect of COM's marshaling layer: Calls across apartments also entail the loading of stub and proxies. Therefore, not only are client requests marshaled across process and machine boundaries, but during intra-apart- ment calls as well. Keep the following situations in mind:

  • If a process has multiple STAs, and an object in one STA wishes to access a COM object in another STA, stubs and proxies are loaded.

  • If an object in the process-wide MTA wishes to work with an object in an STA, stubs and proxies are loaded.

  • If two objects in the same STA want to communicate, stubs and proxies are not loaded.

  • If two objects in the process-wide MTA wish to communicate, stubs and proxies are not loaded.

Figure 7-9 sums up some possible interactions between a process and its apartments, stubs (S), and proxies (P):

click to expand
Figure 7-9: Intra-apartment calls entail stubs and proxies.

Note 

If a client's threading model is incompatible with the coclass's threading model, COM will automatically create stubs and proxies when appropriate. The COM runtime will never refuse to create an object due to incompatible threading.

That concludes our primer of the threading models of COM. Remember that the apartment is a conceptual unit that allows objects written with different levels of thread awareness to live together peacefully. In fact, the apartment is yet another application of location transparency. With this bit of theory behind us, we now turn our attention to the support ATL provides to create thread-aware as well as thread-oblivious coclasses.

Specifying an Object's Threading Model à la ATL

The ATL Object Wizard allows you to specify a COM object's level of thread safety from the Attributes tab. Leaving aside the free threaded marshaler, you are presented with a total of four selections, as seen in Figure 7-10:

click to expand
Figure 7-10: ATL coclasses support all of COM's threading models.

Among other things, these four options map to the possible values placed in the registry to mark a coclass's level of thread awareness. Realize, however, that the ATL naming conventions do not perfectly map to the ThreadingModel value. Assume that we have placed four coclasses in a given DLL server, each one supporting a possible ATL threading option (Single, Apartment, Both, and Free). If we were to examine the generated RGS file for each one, we can see the resulting registry entry.

A coclass marked with the Single threading model will map to (none), and therefore the ThreadingModel value will be empty. Recall that coclasses that do not specify a ThreadingModel value will only be loaded into the initial STA:

ForceRemove {D241B77E-29DF-11D3-B900-0020781238D4} = s 'CoSingle Class' {      ProgID = s 'Threading.CoSingle.1'      VersionIndependentProgID = s 'Threading.CoSingle'      InprocServer32 = s '%MODULE%'      {      }      'TypeLib' = s '{D241B771-29DF-11D3-B900-0020781238D4}' }

ATL coclasses marked as Apartment will load into some STA in the process:

ForceRemove {D241B780-29DF-11D3-B900-0020781238D4} = s 'CoApt Class' {      ProgID = s 'Threading.CoApt.1'      VersionIndependentProgID = s 'Threading.CoApt'      ForceRemove 'Programmable'      InprocServer32 = s '%MODULE%'      {           val ThreadingModel = s 'Apartment'      }      'TypeLib' = s '{D241B771-29DF-11D3-B900-0020781238D4}' }

Coclasses marked as Both or Free will have one of the following lines substituted in their RGS file, and advertise they prefer to be loaded into the process-wide MTA:

val ThreadingModel = s 'Free' val ThreadingModel = s 'Both'

ATL's Core Threading Classes

ATL provides a number of major high-level classes to help you write thread savvy COM objects. Here are two key players, which show up as the parameter passed into CComObjectRootEx<>:

  • CComSingleThreadModel: For objects designed for life in the STA.

  • CComMultiThreadModel: For objects designed for life in the MTA.

Each class defines a number of methods that provide the correct level of threading support for your coclass. The public interface of both classes is identical and you may therefore program against them in the same way. This is very helpful should you ever decide to change your coclass's level of thread awareness. You can simply change the threading template parameter of CComObjectRootEx<> and leave your existing code base unaltered.

Each class provides the Increment() and Decrement() methods to adjust your object's reference counter (m_dwRef) with the appropriate level of thread safety. Each class also provides three typedefs (AutoCriticalSection, CriticalSection, and ThreadModelNoCS) wrapping a Win32 CRITICAL_SECTION (or not, in the case of ThreadModelNoCS) to synchronize your data.

ATL Support for Objects in the STA

ATL objects marked as Single or Apartment will make use of CComSingleThreadModel, specifying the coclass as a member of an STA:

// Coclasses marked as Single or Apartment will be parameterized based off // CComSingleThreadModel. class ATL_NO_VTABLE CCoHexagon :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass< CCoHexagon, &CLSID_CoHexagon >,      public IDraw { ... };

CComSingleThreadModel is defined in <atlbase.h> as the following:

// Provides support for the STA threading model. class CComSingleThreadModel { public:      // Used to adjust your object's reference count.      static ULONG WINAPI Increment(LPLONG p) {return ++(*p);}      static ULONG WINAPI Decrement(LPLONG p) {return --(*p);}      // Typedefs used to wrap a critical section.      // These do nothing in an STA...      typedef CComFakeCriticalSection AutoCriticalSection;      typedef CComFakeCriticalSection CriticalSection;      typedef CComSingleThreadModel ThreadModelNoCS; };

Notice that the Increment() and Decrement() methods of CComSingleThreadModel adjust the object's reference count without regard to thread safety using the C increment (++) and decrement (--) operators. This is a good thing, as objects living in the STA are guaranteed to have a single thread accessing them at any given time, and thus do not need any additional overhead of Win32 atomic locking functions.

CComSingleThreadModel and CComMultiThreadModel each define three typedefs that provide three different ways to work with the underlying CRITICAL_SECTION (or not, if using the ThreadModelNoCS typedef). Exactly what these typedefs map to will depend on the threading model of the coclass.

The first typedef (AutoCriticalSection) is used when you wish to have your critical section automatically initialized and terminated using the constructor and destructor of the typedef-ed class. The CriticalSection typedef forces you to take control over the lifetime of the critical section by calling the Init() and Term() methods manually. Finally, ThreadModelNoCS provides a way to protect your code using a synchronization primitive other than the Win32 CRITICAL_SECTION.

As objects living in the STA have no real interest in thread-safe code, CComSingleThreadModel defines "dummy" critical sections for the AutoCriticalSection and CriticalSection typedefs, represented by CComFakeCriticalSection, which, as you would expect, does nothing:

// Dummy implementations of a critical section for objects in the STA. class CComFakeCriticalSection { public:      void Lock() {}      void Unlock() {}      void Init() {}      void Term() {} };

When CComSingleThreadModel is passed into CComObjectRootEx<>, the typedefs are used to adjust the CRITICAL_SECTION data member maintained by CComObjectRootEx<>:

// How CComObjectRootEx<> makes use of CComSingleThreadModel. template <> class CComObjectRootEx<CComSingleThreadModel> : public CComObjectRootBase { public:      typedef CComSingleThreadModel _ThreadModel;      typedef _ThreadModel::AutoCriticalSection _CritSec;      typedef CComObjectLockT<_ThreadModel> ObjectLock;      ...      void Lock() {}      void Unlock() {} };

The Lock() and Unlock() methods your ATL coclasses inherit make use of the AutoCriticalSection typedef and could now be used to create a thread-safe block of ATL code:

// STA objects use a fake critical section. STDMETHODIMP CCoClass::FooBar() {      // A thread-safe function that uses a fake CRITICAL_SECTION.      Lock();           m_theInt = m_theInt + 10;           if(m_theInt >= 3000)                m_theInt = 0;      Unlock();      return S_OK; }

Of course, Lock() and Unlock() do nothing if your object is living in the STA. However, as both CComSingleThreadModel and CComMultiThreadModel have the same public interface, if you suddenly wish to change the threading model of a coclass, the Lock() and Unlock() methods take on a whole new life. Let's take a look at how ATL provides support for objects choosing to live in the frantic world of the MTA.

ATL Support for Objects in the MTA

If your coclass has been marked as Free or Both you need to be thread safe; hence, your coclass will be parameterized using CComMultiThreadModel:

// Coclasses marked as Free or Both will be parameterized based off // CComMultiThreadModel. class ATL_NO_VTABLE CCoThreadSavvyHexagon :      public CComObjectRootEx< CComMultiThreadModel >,      public CComCoClass< CCoThreadSavvyHexagon,                         &CLSID_CoThreadSavvyHexagon >,      public IDraw { ... };

CComMultiThreadModel is defined in <atlbase.h> as the following. Note this class has the same public interface as CComSingleThreadModel:

// For thread savvy COM objects in the MTA. class CComMultiThreadModel { public:      static ULONG WINAPI Increment(LPLONG p)           {return InterlockedIncrement(p);}      static ULONG WINAPI Decrement(LPLONG p)           {return InterlockedDecrement(p);}      typedef CComAutoCriticalSection AutoCriticalSection;      typedef CComCriticalSection CriticalSection;      typedef CComMultiThreadModelNoCS ThreadModelNoCS; }; 

Here, the Win32 API InterlockedIncrement() and InterlockedDecrement() methods are used to adjust the reference count for the coclass in a thread-safe (atomic) manner rather than the more efficient (but less safe) increment and decrement operators. As we do care a great deal about thread safety when we belong to the MTA, the AutoCriticalSection typedef resolves to a true CRITICAL_SECTION à la CComAutoCriticalSection:

// Objects in the MTA receive a "real" critical section which operate on the // CRITICAL_SECTION data member. class CComAutoCriticalSection { public:      void Lock() {EnterCriticalSection(&m_sec);}      void Unlock() {LeaveCriticalSection(&m_sec);}      CComAutoCriticalSection() {InitializeCriticalSection(&m_sec);}      ~CComAutoCriticalSection() {DeleteCriticalSection(&m_sec);}      CRITICAL_SECTION m_sec; };

Using CComMultiThreadModel, Lock() and Unlock() operate on a true critical section:

// Locking down our critical data using ATL. STDMETHODIMP CCoClass::FooBar() {      // A thread-safe function using a CRITICAL_SECTION.      Lock();           m_theInt = m_theInt + 10;           if(m_theInt >= 3000)                m_theInt = 0;      Unlock();      return S_OK; }

So then to recap, ATL provides a number of threading classes that provide the most efficient reference counting scheme for your coclass, based on the object's level of thread awareness. Objects in the STA will use the ++ and -- operators, while objects in the MTA will make use of InterlockedIncrement() and InterlockedDecrement().

As well, your coclass will receive the Lock() and Unlock() methods to protect your state data from concurrency issues. The implementation of these methods depends on the parameter of CComObjectRootEx<>. CComSingleThreadModel defines a no-op critical section via CComFakeCriticalSection. CComMultiThreadModel defines a "real" critical section using CComAutoCriticalSection.

Understand that ATL source code is "thread safe enough" as well. As an ATL developer, you will never need to concern yourself with the thread safety of framework code. ATL state and global data is protected from access by multiple threads, using CComGlobalsThreadModel. This is not a new class, but a typedef that resolves to either CComSingleThreadModel or CComMultiThreadModel, based on the default threading of the ATL server.

An ATL Server's Default Threading Support

Every project generated using the ATL COM AppWizard maintains what is known as the "default threading model" for the server. This default is used to account for all threading details you don't directly have too much concern over (such as how ATL locks its global data). If you examine your precompiled header (stdafx.h) you will see the default ATL threading classes are determined based on a handful of ATL defined constants. These flags are used to specify which ATL classes will be used by the framework to work with global and ATL state data, as seen in <atlbase.h>:

// These flags are used to determine how ATL should take care of its own data. // #if defined(_ATL_SINGLE_THREADED)      typedef CComSingleThreadModel CComObjectThreadModel;      typedef CComSingleThreadModel CComGlobalsThreadModel; #elif defined(_ATL_APARTMENT_THREADED)      typedef CComSingleThreadModel CComObjectThreadModel;      typedef CComMultiThreadModel CComGlobalsThreadModel; #else     // _ATL_FREE_THREADED      typedef CComMultiThreadModel CComObjectThreadModel;      typedef CComMultiThreadModel CComGlobalsThreadModel; #endif

As you can guess, if you replace the initial flag found in your precompiled header file (_ATL_APARTMENT_THREADED) you would change how ATL should handle these matters. If you have any MTA-aware objects in your server, you should change this flag to _ATL_FREE_THREADED to force ATL to use more thread savvy classes to handle access to ATL's global and static data.

Threading Summary

Threading is a complex issue, even in a typical Win32 application using no COM objects whatsoever. As we have seen, a single process always has a primary thread (WinMain()) which may spawn additional threads using CreateThread() or a similar threading function. When a process has multiple threads, the developer must ensure that all shared data is thread safe, making use of a locking mechanism such as the CRITICAL_SECTION.

Once you inject COM into the picture, threading becomes even more complex. Given the fact that your COM objects can be used by single-threaded or multithreaded processes, we must ensure our coclasses are thread safe as well. However, in COM, the concept of an apartment helps ensure that COM objects receive automatic synchronization if the object is marked to live in the STA. If an object is marked to live in the MTA, it is again the responsibility of the developer to ensure all shared data is thread safe. As you have also seen, stubs and proxies are loaded for any intra-apartment calls.

Finally, when you use ATL to build your coclasses, you receive Lock() and Unlock() methods that operate on a true CRITICAL_SECTION (for objects in the MTA) or a fake critical section (for objects in the STA). If you have no interest (or time) to create thread savvy coclasses, mark your objects as apartment threaded. Your performance is still reasonably good. If you love threads (or have lots of spare time), set up your objects to live in the MTA, and Lock() and Unlock() accordingly.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net