Threading Model Support


Just Enough Thread Safety

The thread-safe implementation of AddRef and Release shown previously might be overkill for your COM objects. For example, if instances of a specific class will live in only a single-threaded apartment, there's no reason to use the thread-safe Win32 functions InterlockedIncrement and InterlockedDecrement. For single-threaded objects, the following implementation of AddRef and Release is more efficient:

class Penquin { ...   ULONG AddRef()   { return ++m_cRef; }   ULONG Release() {       ULONG l = m_cRef;       if( l == 0 ) delete this;       return l;   } ... }; 


Using the thread-safe Win32 functions also works for single-threaded objects, but unnecessary thread safety requires extra overhead. For this reason, ATL provides three classes, CComSingleThreadModel, CComMultiThreadModel, and CComMultiThreadModelNoCS. These classes provide two static member functions, Increment and Decrement, for abstracting away the differences between managing an object's lifetime count in a multithreaded manner versus a single-threaded one. The two versions of these functions are as follows (notice that both CComMultiThreadModel and CComMultiThreadModelNoCS have identical implementations of these functions):

class CComSingleThreadModel {                                                   static ULONG WINAPI Increment(LPLONG p) { return ++(*p); }                    static ULONG WINAPI Decrement(LPLONG p) { return (*p); }                     ...                                                                         };                                                                            class CComMultiThreadModel {                                                    static ULONG WINAPI Increment(LPLONG p) { return InterlockedIncrement(p); }   static ULONG WINAPI Decrement(LPLONG p) { return InterlockedDecrement(p); }   ...                                                                         };                                                                            class CComMultiThreadModelNoCS {                                                static ULONG WINAPI Increment(LPLONG p) { return InterlockedIncrement(p); }   static ULONG WINAPI Decrement(LPLONG p) { return InterlockedDecrement(p); }   ...                                                                         };                                                                            


Using these classes, you can parameterize[1] the class to give a "just thread-safe enough" AddRef and Release implementation:

[1] For an introduction to C++ templates and how they're used in ATL, see Appendix A, "C++ Templates by Example.

template <typename ThreadModel> class Penquin { ...   ULONG AddRef()   { return ThreadModel::Increment(&m_cRef); }   ULONG Release() {       ULONG l = ThreadModel::Decrement(&m_cRef);       if( l == 0 ) delete this;       return l;   } ... }; 


Now, based on our requirements for the CPenguin class, we can make it just thread-safe enough by supplying the threading model class as a template parameter:

// Let's make a thread-safe CPenguin CPenguin* pobj = new CPenguin<CComMultiThreadModel>( ); 


Instance Data Synchronization

When you create a thread-safe object, protecting the object's reference count isn't enough. You also have to protect the member data from multithreaded access. One popular method for protecting data that multiple threads can access is to use a Win32 critical section object, as shown here:

template <typename ThreadModel> class CPenguin { public:   CPenguin() {     ServerLock();     InitializeCriticalSection(&m_cs);   }   ~CPenguin() { ServerUnlock(); DeleteCriticalSection(&m_cs); }   // IBird   STDMETHODIMP get_Wingspan(long* pnWingspan) {     Lock(); // Lock out other threads during data read     *pnWingSpan = m_nWingspan;     Unlock();     return S_OK;   }   STDMETHODIMP put_Wingspan(long nWingspan) {     Lock(); // Lock out other threads during data write     m_nWingspan = nWingspan;     Unlock();     return S_OK;   }   ... private:   CRITICALSECTION m_cs;   void Lock() { EnterCriticalSection(&m_cs); }   void Unlock() { LeaveCriticalSection(&m_cs); } }; 


Notice that before reading or writing any member data, the CPenguin object enters the critical section, locking out access by other threads. This coarse-grained, object-level locking keeps the scheduler from swapping in another thread that could corrupt the data members during a read or a write on the original thread. However, object-level locking doesn't give you as much concurrency as you might like. If you have only one critical section per object, one thread might be blocked trying to increment the reference count while another is updating an unrelated member variable. A greater degree of concurrency requires more critical sections, allowing one thread to access one data member while a second thread accesses another. Be careful using this kind of finer-grained synchronizationit often leads to deadlock:

class CZax : public IZax { public:   ...   // IZax   STDMETHODIMP GoNorth() {       EnterCriticalSection(&m_cs1); // Enter cs1...       EnterCriticalSection(&m_cs2); // ...then enter cs2       // Go north...       LeaveCriticalSection(&m_cs2);       LeaveCriticalSection(&m_cs1);   }   STDMETHODIMP GoSouth() {       EnterCriticalSection(&m_cs2); // Enter cs2...       EnterCriticalSection(&m_cs1); // ...then enter cs1       // Go south...       LeaveCriticalSection(&m_cs1);       LeaveCriticalSection(&m_cs2);   }   ... private:     CRITICAL_SECTION m_cs1;     CRITICAL_SECTION m_cs2; }; 


Imagine that the scheduler let the northbound Zax[2] thread enter the first critical section and then swapped in the southbound Zax thread to enter the second critical section. If this happened, neither Zax could enter the other critical section; therefore, neither Zax thread would be able to proceed. This would leave them deadlocked while the world went on without them. Try to avoid this.[3]

[2] The Sneeches and Other Stories, by Theodor Geisel (aka Dr. Seuss).

[3] For more guidance on what to do about deadlocks, read Win32 Multithreaded Programming (O'Reilly and Associates, 1997), by Mike Woodring and Aaron Cohen.

Whether you decide to use object-level locking or finer-grained locking, critical sections are handy. ATL provides four class wrappers that simplify their use: CComCriticalSection, CComAutoCriticalSection, CComSafeDeleteCriticalSection, and CComAutoDeleteCriticalSection.

class CComCriticalSection {                                                  public:                                                                          CComCriticalSection() {                                                          memset(&m_sec, 0, sizeof(CRITICAL_SECTION));                             }                                                                            ~CComCriticalSection() { }                                                   HRESULT Lock() {                                                                 EnterCriticalSection(&m_sec);                                                return S_OK;                                                             }                                                                            HRESULT Unlock() {                                                               LeaveCriticalSection(&m_sec);                                                return S_OK;                                                             }                                                                            HRESULT Init() {                                                                 HRESULT hRes = E_FAIL;                                                       __try {                                                                          InitializeCriticalSection(&m_sec);                                           hRes = S_OK;                                                             }                                                                            // structured exception may be raised in                                     // low memory situations                                                     __except(STATUS_NO_MEMORY == GetExceptionCode()) {                               hRes = E_OUTOFMEMORY;                                                    }                                                                            return hRes;                                                             }                                                                            HRESULT Term() {                                                                 DeleteCriticalSection(&m_sec);                                               return S_OK;                                                             }                                                                            CRITICAL_SECTION m_sec;                                                  };                                                                           class CComAutoCriticalSection : public CComCriticalSection {                 public:                                                                          CComAutoCriticalSection() {                                                      HRESULT hr = CComCriticalSection::Init();                                    if (FAILED(hr))                                                                  AtlThrow(hr);                                                        }                                                                            ~CComAutoCriticalSection() {                                                     CComCriticalSection::Term();                                             }                                                                        private:                                                                         // Not implemented. CComAutoCriticalSection::Init                            // should never be called                                                    HRESULT Init();                                                              // Not implemented. CComAutoCriticalSection::Term                            // should never be called                                                    HRESULT Term();                                                          };                                                                           class CComSafeDeleteCriticalSection                                              : public CComCriticalSection {                                           public:                                                                          CComSafeDeleteCriticalSection(): m_bInitialized(false) { }                   ~CComSafeDeleteCriticalSection() {                                               if (!m_bInitialized) { return; }                                             m_bInitialized = false;                                                      CComCriticalSection::Term();                                             }                                                                            HRESULT Init() {                                                                 ATLASSERT( !m_bInitialized );                                                HRESULT hr = CComCriticalSection::Init();                                    if (SUCCEEDED(hr)) {                                                             m_bInitialized = true;                                                   }                                                                            return hr;                                                               }                                                                            HRESULT Term() {                                                                 if (!m_bInitialized) { return S_OK; }                                        m_bInitialized = false;                                                      return CComCriticalSection::Term();                                      }                                                                            HRESULT Lock() {                                                                 ATLASSUME(m_bInitialized);                                                   return CComCriticalSection::Lock();                                      }                                                                        private:                                                                         bool m_bInitialized;                                                     };                                                                           class CComAutoDeleteCriticalSection : public CComSafeDeleteCriticalSection { private:                                                                         // CComAutoDeleteCriticalSection::Term should never be called                HRESULT Term() ;                                                         };                                                                           


Notice that CComCriticalSection does not use its constructor or destructor to initialize and delete the contained critical section. Instead, it contains Init and Term functions for this purpose. CComAutoCriticalSection, on the other hand, is easier to use because it automatically creates the critical section in its constructor and destroys it in the destructor.

CComSafeDeleteCriticalSection does half that job; it doesn't create the critical section until the Init method is called, but it always deletes the critical section (if it exists) in the destructor. You also have the option of manually calling Term if you want to explicitly delete the critical section ahead of the object's destruction. CComAutoDeleteCriticalSection, on the other hand, blocks the Term method by simply declaring it but never defining it; calling CComAutoDeleteCriticalSection::Term gives you a linker error. These classes were useful before ATL was consistent about supporting construction for global and static variables, but these classes are largely around for historical reasons at this point; you should prefer CComAutoCriticalSection.

Using a CComAutoCriticalSection in our CPenguin class simplifies the code a bit:

template <typename ThreadModel> class CPenguin { public:   // IBird methods Lock() and Unlock() as before... ... private:   CComAutoCriticalSection m_cs;   void Lock() { m_cs.Lock(); }   void Unlock() { m_cs.Unlock(); } }; 


Note that with both CComAutoCriticalSection and CComCriticalSection, the user must take care to explicitly call Unlock before leaving a section of code that has been protected by a call to Lock. In the presence of code that might throw exceptions (which a great deal of ATL framework code now does), this can be difficult to do because each piece of code that can throw an exception represents a possible exit point from the function. CComCritSecLock addresses this issue by automatically locking and unlocking in its constructor and destructor. CComCritSecLock is parameterized by the lock type so that it can serve as a wrapper for CComCriticalSection or CComAutoCriticalSection.

template< class TLock >                                       class CComCritSecLock {                                       public:                                                             CComCritSecLock( TLock& cs, bool bInitialLock = true );       ~CComCritSecLock() ;                                          HRESULT Lock() ;                                              void Unlock() ;                                         // Implementation                                             private:                                                            TLock& m_cs;                                                  bool m_bLocked;                                         ...                                                           };                                                            template< class TLock >                                       inline CComCritSecLock< TLock >::CComCritSecLock(                 TLock& cs,bool bInitialLock )                                 : m_cs( cs ), m_bLocked( false ) {                              if( bInitialLock ) {                                                HRESULT hr;                                                   hr = Lock();                                                  if( FAILED( hr ) ) { AtlThrow( hr ); }                  }                                                       }                                                             template< class TLock >                                       inline CComCritSecLock< TLock >::~CComCritSecLock() {               if( m_bLocked ) { Unlock(); }                           }                                                             template< class TLock >                                       inline HRESULT CComCritSecLock< TLock >::Lock() {                   HRESULT hr;                                                   ATLASSERT( !m_bLocked );                                      hr = m_cs.Lock();                                             if( FAILED( hr ) ) { return( hr ); }                          m_bLocked = true;                                             return( S_OK );                                         }                                                             template< class TLock >                                       inline void CComCritSecLock< TLock >::Unlock() {                    ATLASSERT( m_bLocked );                                       m_cs.Unlock();                                                m_bLocked = false;                                      }                                                             


If the bInitialLock parameter to the constructor is true, the contained critical section is locked upon construction. In normal use on the stack, this is exactly what you want, which is why true is the default. However, as usual with constructors, if something goes wrong, you don't have an easy way to return the failure code. If you need to know whether the lock failed, you can pass false instead and then call Lock explicitly. Lock returns the HRESULT from the lock operation. This class ensures that the contained critical section is unlocked whenever an instance of this class leaves scope because the destructor automatically attempts to call Unlock if it detects that the instance is currently locked.

Notice that our CPenguin class is still parameterized by the threading model. There's no sense in protecting our member variables in the single-threaded case. Instead, it would be handy to have another critical section class that could be used in place of CComCriticalSection or CComAutoCriticalSection. ATL provides the CComFakeCriticalSection class for this purpose:

class CComFakeCriticalSection {       public:                                   HRESULT Lock() { return S_OK; }       HRESULT Unlock() { return S_OK; }     HRESULT Init() { return S_OK; }       HRESULT Term() { return S_OK; }   };                                    


Given CComFakeCriticalSection, we could further parameterize the CPenguin class by adding another template parameter, but this is unnecessary. The ATL threading model classes already contain type definitions that map to a real or fake critical section, based on whether you're doing single or multithreading:

class CcomSingleThreadModel {                                                 public:                                                                           static ULONG WINAPI Increment(LPLONG p) {return ++(*p);}                      static ULONG WINAPI Decrement(LPLONG p) {return (*p);}                       typedef CComFakeCriticalSection AutoCriticalSection;                          typedef CComFakeCriticalSection AutoDeleteCriticalSection;                    typedef CComFakeCriticalSection CriticalSection;                              typedef CComSingleThreadModel ThreadModelNoCS;                            };                                                                            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 CComAutoDeleteCriticalSection                                             AutoDeleteCriticalSection;                                                typedef CComCriticalSection CriticalSection;                                  typedef CComMultiThreadModelNoCS ThreadModelNoCS;                         };                                                                            class CcomMultiThreadModelNoCS {                                              public:                                                                           static ULONG WINAPI Increment(LPLONG p) {return InterlockedIncrement(p);}     static ULONG WINAPI Decrement(LPLONG p) {return InterlockedDecrement(p);}     typedef CComFakeCriticalSection AutoCriticalSection;                          typedef CComFakeCriticalSection AutoDeleteCriticalSection;                    typedef CComFakeCriticalSection CriticalSection;                              typedef CComMultiThreadModelNoCS ThreadModelNoCS;                         };                                                                            


These type definitions enable us to make the CPenguin class just thread safe enough for both the object's reference count and course-grained object synchronization:

template <typename ThreadingModel> class CPenguin { public:     // IBird methods as before... ... private:     ThreadingModel::AutoCriticalSection m_cs;     void Lock() { m_cs.Lock(); }     void Unlock() { m_cs.Unlock(); } }; 


This technique enables you to provide the compiler with operations that are just thread safe enough. If the threading model is CComSingleThreadModel, the calls to Increment and Decrement resolve to operator++ and operator, and the Lock and Unlock calls resolve to empty inline functions.

If the threading model is CComMultiThreadModel, the calls to Increment and Decrement resolve to calls to InterlockedIncrement and InterlockedDecrement. The Lock and Unlock calls resolve to calls to EnterCriticalSection and LeaveCriticalSection.

Finally, if the model is CComMultiThreadModelNoCS, the calls to Increment and Decrement are thread safe, but the critical section is fake, just as with CComSingleThreadModel. CComMultiThreadModelNoCS is designed for multithreaded objects that eschew object-level locking in favor of a more fine-grained scheme. Table 4.1 shows how the code is expanded based on the threading model class you use:

Table 4.1. Expanded Code Based on Threading Model Class
 

CcomSingleThreadModel

CComMultiThreadModel

CComMultiThreadModelNoCS

TM::Increment

++

Interlocked-Increment

Interlocked-Increment

TM::Decrement

Interlocked-Decrement

Interlocked-Decrement

TM::AutoCriticalSection::Lock

(Nothing)

EnterCritical-Section

(Nothing)

TM::AutoCriticalSection::Unlock

(Nothing)

LeaveCritical-Section

(Nothing)


The Server's Default Threading Model

ATL-based servers have a concept of a "default" threading model for things that you don't specify directly. To set the server's default threading model, you define one of the following symbols: _ATL_SINGLE_THREADED, _ATL_APARTMENT_THREADED, or _ATL_FREE_THREADED. If you don't specify one of these symbols, ATL assumes _ATL_FREE_THREADED. However, the ATL Project Wizard defines _ATL_APARTMENT_THREADED in the generated stdafx.h file. ATL uses these symbols to define two type definitions:

#if defined(_ATL_SINGLE_THREADED)                         ...                                                           typedef CComSingleThreadModel CComObjectThreadModel;      typedef CComSingleThreadModel CComGlobalsThreadModel; #elif defined(_ATL_APARTMENT_THREADED)                    ...                                                           typedef CComSingleThreadModel CComObjectThreadModel;      typedef CComMultiThreadModel CComGlobalsThreadModel;  #elif defined(_ATL_FREE_THREADED)                         ...                                                           typedef CComMultiThreadModel CComObjectThreadModel;       typedef CComMultiThreadModel CComGlobalsThreadModel;  ...                                                       #endif                                                    


Internally, ATL uses CComObjectThreadModel to protect instance data and CComGlobalsThreadModel to protect global and static data. Because the usage is difficult to override in some cases, you should make sure that ATL is compiled using the most protective threading model of any of the classes in your server. In practice, this means you should change the wizard-generated _ATL_APARTMENT_THREADED symbol to _ATL_FREE_THREADED if you have even one multithreaded class in your server.




ATL Internals. Working with ATL 8
ATL Internals: Working with ATL 8 (2nd Edition)
ISBN: 0321159624
EAN: 2147483647
Year: 2004
Pages: 172

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