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 the class to give a "just thread-safe
enough" AddRef and Release implementation:
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 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.
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.
|