Page #81 (Cross-Context Access)

< BACK  NEXT >
[oR]

Developing Thread-Safe COM Code

At this point, we have a good understanding of multithreaded programming issues as well as the intricacies of COM apartments and marshaling. So let s take a look at some common techniques to develop apartment-safe and thread-safe code.

Shared Data Conflicts

Earlier, I mentioned that if a memory location or any other resource will be accessed concurrently from more than one thread, the developer has to provide some explicit mechanism to synchronize access to such a shared resource.

Using a Main STA

The most convenient solution is to let COM synchronize the access by not specifying the ThreadingModel attribute on the class. This forces COM to create all the objects in the main STA. As only one thread will ever access the main STA, there is no sharing of data between multiple threads.

While this solution works, there is a performance penalty. Every method call has to be marshaled back to the main STA thread.

Using an STA

The next best thing to do would be to mark the ThreadingModel as Apartment. COM places each object provided by the component in an STA, thereby achieving serialization on each object.

While this protects the state of the object from concurrent access, any data that is shared among multiple objects is not protected. This is because objects from the components can be created in multiple STAs. Multiple STA threads can access a shared data concurrently. It is the responsibility of the developer to use proper synchronization primitives to protect shared data.

Let s see how we can use a Win32 synchronization primitive to protect a shared resource.

Perhaps the most efficient Win32 synchronization primitive is a critical section. To lock access to a resource, a thread should enter a critical section. Once done using the resource, the thread should unlock access to the resource by leaving the critical section.

ATL provides a class, CComAutoCriticalSection, that simplifies the usage of a critical section. Here is how you can protect a global variable, g_nCount, using this class.

The first step is to declare a global variable of type CComAutoCriticalSection, as shown below:

 CComAutoCriticalSection g_myCS;  long g_nCount = 500; 

Each time g_nCount is accessed, Lock and Unlock methods on CComAutoCriticalSection should be called, as illustrated in the following code snippet:

 STDMETHODIMP CMyCount::GetCount(long *plCount)  {   g_myCS.Lock();    *plCount = g_nCount;    g_myCS.Unlock();    return S_OK;  }  STDMETHODIMP CMyCount::IncrementCount(long nValue)  {   g_myCS.Lock();    g_nCount += nValue;    g_myCS.Unlock();    return S_OK;  } 

Any number of variables can be grouped together and accessed within just one critical section. Mutually exclusive groups of variables may be enclosed in separate critical sections to provide a finer level of granularity.

graphics/01icon01.gif

Recall from Chapter 3 that the class factory that is returned via DllGetClassObject is a global singleton and is typically created on the first call to DllGetClassObject. As DllGetClassObject can be invoked concurrently from two different STA threads, the creation of the class factory object has to be protected. Fortunately, ATL does this for us.

A similar condition applies to the DllGetUnloadNow, the function that keeps track of outstanding object references via a global variable. Once again, ATL provides thread-safe implementation of this function.


Though a critical section is quite efficient, Win32 provides a simpler and more efficient mechanism for synchronizing access to a single variable of data type LONG. The mechanism uses the InterlockedXX family of APIs. Using these APIs, for example, our code can be simplified as follows:

 STDMETHODIMP CMyCount::GetCount(long *plCount)  {   *plCount = g_nCount;    return S_OK;  }  STDMETHODIMP CMyCount::IncrementCount(long nValue)  {   ::InterlockedExchangeAdd(&g_nCount, nValue);    return S_OK;  } 

graphics/01icon01.gif

The InterlockedXX functions are used only if the value needs to be modified, not if the value just needs to be read.


In my CPL toolkit, I have provided a class, CCPLWinAtomicCounter, that abstracts Interlocked APIs. You can use a variable of this class type as a regular LONG variable, except that it is protected from shared access:

 CCPLWinAtomicCounter g_nCount;  ...  STDMETHODIMP CMyCount::GetCount(long *plCount)  {   *plCount = g_nCount;    return S_OK;  }  STDMETHODIMP CMyCount::IncrementCount(long nValue)  {   g_nCount += nValue;    return S_OK;  } 
Using Activities

If the class code does not have any thread affinity, another approach would be to mark the class as Synchronization=Required. This will result in only one thread accessing the object at any time. This case is similar to STA; although the state of the object is protected implicitly, any shared data between multiple objects needs to be protected explicitly.

The activity logic fails when two threads try to enter the same activity from two different processes. An activity is locked only process-wide; not machine or domain wide. This decision was made on the premise that the cost associated with providing a fully distributed activity protection outweighs its benefits. As a result, if two objects A and B from two different processes are part of the same activity, and calls A::X and B::Y are made on them from different threads, it is entirely possible that the two calls will execute concurrently. Worse yet, if objects A and B were to call each other, a deadlock would almost certainly ensue as X and Y would be part of two different causalities and would not be considered nested calls.

To avoid such a problem, multiple clients should not share an object, especially across process boundaries. If two different clients need to access the same piece of data, they should do so by creating two distinct object identities that access the same data state.

Using a TNA or an MTA

Now we are getting ready to get our hands dirty. Instead of relying on COM to provide synchronization, we are indicating that we will provide our own complete synchronization solution.

Using an MTA or a TNA, two method calls on an object can execute concurrently. Therefore, not only the state of the global data needs protection, even the state of the object specific data has to be protected from concurrent access.

In Chapter 3, we used a LONG variable to keep track of the reference count on an object:

 STDMETHODIMP_(ULONG) CVcr::AddRef()  {   return (++m_lRefCount);  }  STDMETHODIMP_(ULONG) CVcr::Release()  {   ULONG lRetVal = ( m_lRefCount);    if (0 == lRetVal) {     delete this;    }    return lRetVal;  } 

While the code served its purpose in explaining the concept of reference counting, it is not thread-safe under the MTA or TNA. The shared variable, m_lRefCount, needs protection in case the two methods execute concurrently. A standard technique is to use InterlockedIncrement and InterlockedDecrement APIs for reference counting. Fortunately, once again ATL-generated code rescues us from this labor.

How about protecting the state of an individual object?

Synchronization primitives such as a critical section or a mutex can be used to protect data. However, one has to be careful using these primitives. Improper use or careless use can spell trouble. Let s see how.

The code fragment below shows a simple ATL-based implementation class that supports an interface to set a person s first and last name.

 class CPerson :    ...  {   ...  // IPerson  public:    STDMETHOD(LockAccess)();    STDMETHOD(SetFirstName)(/*[in]*/ BSTR bsFirstName);    STDMETHOD(SetLastName)(/*[in]*/ BSTR bsLastName);    STDMETHOD(UnlockAccess)();  private:    CComAutoCriticalSection m_CS;    CComBSTR m_bsFirstName;    CComBSTR m_bsLastName;  };  STDMETHODIMP CPerson::LockAccess()  {   m_CS.Lock();    return S_OK;  }  STDMETHODIMP CPerson::SetFirstName(BSTR bsFirstName)  {   m_bsFirstName = bsFirstName;    return S_OK;  }  STDMETHODIMP CPerson::SetLastName(BSTR bsLastName)  {   m_bsLastName = bsLastName;    return S_OK;  }  STDMETHODIMP CPerson::UnlockAccess()  {   m_CS.Unlock();    return S_OK;  } 

The goal is to let the client explicitly lock access to the object s state before updating the first and the last name, and unlocking the access after finishing the update.

While the code looks simple and the logic looks clean, there is a major problem with the code. It, in all likelyhood, won t work under an MTA.

Recall that, under an MTA, an incoming call is serviced by an arbitrary thread from the RPC thread cache. The call to LockAccess may be serviced by one RPC thread and the call to UnlockAccess may be serviced by another RPC thread. The problem is, the synchronization primitives such as critical sections and mutexes have thread affinity. One cannot lock a critical section on one thread and unlock it in some other thread.

So, let s get smarter lock the access and update the data within the same call. The following code fragment shows the revised logic:

 STDMETHODIMP CPerson::SetName(BSTR bsFirstName, BSTR bsLastName)  {   m_CS.Lock();    m_bsFirstName = bsFirstName;    m_bsLastName = bsLastName;    return S_OK;  } 

This is much better. Right?

Except for one little problem we forgot to call Unlock on the critical section. As a result, the resource stays locked. Another call to method SetName, or any other method call that accesses the data members, may come on a different RPC thread and will get blocked trying to lock the resource.

While the case shown above is a trivial one, it is easy to make such a mistake in a complex software code. For example, if a method has many return paths, the developer can overlook calling Unlock on one of the return paths.

There is yet another possibility of human error. Once the developers decide to associate a synchronization primitive with some resource, that resource should never be accessed without locking the primitive first. In a complex component with hundreds of methods that access some internal data, it is easy to forget locking the resource in one or two methods. And the bug will go undetected. The C++ compiler can only catch syntactic errors; it cannot catch a logical error.

To eliminate the possibility of making such logical errors, I have developed a template class, CCPLWinSharedResource, that makes a resource safe from concurrent access. The data that needs to be protected can be specified as a parameter to the template. Any valid C data type or structure can be passed as a parameter. The following code fragment shows how to declare a thread-safe variable using this template class:

 class CPerson :    ...  private:    struct PERSONINFO {     CComBSTR bsFirstName;      CComBSTR bsLastName;    };    CCPLWinSharedResource<PERSONINFO> m_Info;  }; 

In this code, the PERSONINFO structure is embedded within the variable m_Info. The only way to access the structure is by taking ownership of m_Info. This is done by declaring a local variable of type CCPLWinSharedResource<T>::GUARD, as shown below:

 STDMETHODIMP CPerson::SetName(BSTR bsFirstName, BSTR bsLastName)  {   CCPLWinSharedResource<PERSONINFO>::GUARD guard(m_Info);    PERSONINFO& info = guard;    info.bsFirstName = bsFirstName;    info.bsLastName = bsLastName;    return S_OK;  } 

Internally, CCPLWinSharedResource uses a critical section to protect data. By declaring the local variable of type CCPLWinSharedResource<PERSONINFO>::GUARD, the critical section is locked. The lock is released when the local variable goes out of the scope.

Using the CCPLWinSharedResource template removes the possibility of human errors we discussed earlier, as follows:

  • The compiler will not let you access the embedded data directly. To access the data, you have to declare a local variable of GUARD type.

    This will result in locking the resource.

  • The resource will be unlocked automatically when the variable goes out of scope. You do need to explicitly unlock it.

The template class is part of the CPL toolkit and is included on the CD.

Handling Reentrancy

We know that when an outgoing call is made from an STA, COM creates a message pump inside the channel while waiting for the ORPC response. Besides processing any non-COM-related Windows messages, the message pump also services any incoming ORPC requests (as Windows messages).

Servicing incoming ORPC requests while making an outgoing call makes the STA code reentrant.

By default, the channel will allow all the incoming ORPC requests to be serviced while the client thread waits for an ORPC response. However, COM provides a way to install a custom message filter for the thread. A message filter is a per-STA COM object that implements an interface IMessageFilter. A developer can install a custom message filter using the COM API CoRegisterMessageFilter. Implementing a message filter is relatively straightforward and is left as an exercise for the readers.

Note that message filters are unique to STAs. As there are no concurrency guarantees for the MTA, COM doesn t provide any support for reentrancy. It is up to the developers to handle reentrancy issues.

One problem with reentrancy under MTAs is the possibility of a deadlock. Consider the case when an outbound method call to another object is made from an MTA while holding a physical lock and the other object calls back into the MTA, as shown in the following code fragment:

 STDMETHODIMP CPerson::GetName(BSTR* pbsFirstName,    BSTR* pbsLastName)  {   m_CS.Lock();    *pbsFirstName = m_bsFirstName.Copy();    *pbsLastName = m_bsLastName.Copy();    m_CS.Unlock();    return S_OK;  }  STDMETHODIMP CPerson::SetName(BSTR bsFirstName, BSTR bsLastName)  {   m_CS.Lock();    m_bsFirstName = bsFirstName;    m_bsLastName = bsLastName;    // inform another object of the name change    m_spAdmin->NameChange(this);    m_CS.Unlock();    return S_OK;  }  STDMETHODIMP CAdmin::NameChange(IPerson* pPerson)  {   CComBSTR bsFirstName, bsLastName;    pPerson->GetName(&bsFirstName, &bsLastName);    return S_OK;  } 

When CPerson::SetName is called, the method locks the resource, updates the name, and calls CAdmin::NameChange to inform the CAdmin object of the name change. When the CAdmin object calls back into CPerson::GetName, a thread from the RPC thread cache services the call. As the resource is already locked on the outgoing thread, a deadlock occurs.

There are many ways to deal with such deadlocks. The developers have to deal with them on a case-by-case basis. For example, in the above code, a little rearrangement of the code could avoid the deadlock. The revised code snippet is shown below:

 STDMETHODIMP CPerson::SetName(BSTR bsFirstName, BSTR bsLastName)  {   m_CS.Lock();    m_bsFirstName = bsFirstName;    m_bsLastName = bsLastName;    m_CS.Unlock(); // unlock the resource first    // inform another object of the name change    m_spAdmin->NameChange(this);    return S_OK;  } 

Another possibility of avoiding deadlocks is to configure a component to run under an activity. In this case, COM provides infrastructural support to prevent deadlocks, as we covered earlier in the chapter. However, if your object will run under an activity, be very careful of using any explicit synchronization mechanism in your code. If the activity spans multiple processes, though there is just one logical thread, there is a possibility that multiple physical threads could enter an activity, and your synchronization mechanism may result in a deadlock.

Note that deadlocks are not limited to MTAs. As TNAs too are reentrant, there is a possibility of a deadlock here.

Waiting for an Event

We know that, in an STA, if an interface method call takes considerable time to execute, no incoming ORPC requests will be processed. Furthermore, since no UI-related Windows messages would get processed, the UI will appear to freeze. Therefore, any interface method in an STA should never do a blocking wait (using WaitForSingleObject or WaitForMultipleObjects, for example). Moreover, if the method is expected to take a considerable time to execute, then the method should implement some variant of the message pump.

What if you do have to wait for an event to be signaled?

To handle this problem, the SDK provides an API called CoWaitForMultipleHandles. This function is similar in functionality to its Win32 counterpart WaitForMultipleObjectsEx. The difference is that this function internally runs a message pump, besides waiting for an event to be signaled. The function also uses the calling thread s message filter to control how pending calls will be handled.

CoWaitForMultipleHandles can be used with any apartment type. The function is smart enough to recognize if the current apartment is an MTA and, if so, disables the message pump. If the apartment is an STA or TNA, the message pump remains enabled.

The CPL toolkit class CCPLWinThread (on the CD) uses CoWaitForMultipleHandles internally. I have also included a program on the CD to demonstrate the use of CoWaitForMultipleHandles.

graphics/01icon01.gif

You may be wondering why we need message pumps under TNA. Recall that a method call from an STA can directly enter a TNA. Therefore, any class that is implemented under a TNA is subject to the same rules as that of an STA. Like STA, if an interface method takes considerable time to execute under a TNA, the implementation should introduce a message pump.


Sharing State Across Multiple Objects

Many times it is desirable to share one or more properties (values) across many objects within a process. For example, an application may control the feature-set it exposes to the customers by means of a license key. Each component within the application may individually wish to control the feature-set by accessing the license key.

If all the objects that are interested in sharing the properties belong to the same COM server, the problem can be easily solved. A global variable can be used to contain each property that needs to be shared. Of course, the global variables need to be protected from concurrent access.

The technique of using global variables, however, will not work if the properties need to be shared across various components (COM servers). The data segment (where the global variables are stored) for each DLL is generally private to the DLL. Therefore, a global variable from one component cannot be accessed directly from another component.

One way to get around this problem is to ensure that all the global variables reside within one component. All other components can access the properties by means of an interface on the first component.

COM Singleton

The above-mentioned technique can be improvised by ensuring that the component that holds the properties is a singleton, that is, the class factory for the COM class returns the same object each time the client calls CoCreateInstance (or other similar APIs). This eliminates the need for using global variables to store properties.

ATL makes it very convenient to develop a singleton COM class. It requires that just one macro, DECLARE_CLASSFACTORY_SINGLETON, be added to the implementation.

The following code snippet shows a singleton COM server that exposes an interface to access a property called LicenseKey:

 // File MySingleton.h  class CMySingleton :    ...  {   ...  BEGIN_COM_MAP(CMySingleton)    COM_INTERFACE_ENTRY(IMySingleton)  END_COM_MAP()  DECLARE_CLASSFACTORY_SINGLETON(CMySingleton)  // IMySingleton  public:    STDMETHOD(get_LicenseKey)(/*[out, retval]*/ BSTR *pVal);    STDMETHOD(put_LicenseKey)(/*[in]*/ BSTR newVal);  private:    // variable to hold the license key    CCPLWinSharedResource<CComBSTR> m_bsLicenseKey;  };  // File MySingleton.cpp  STDMETHODIMP CMySingleton::get_LicenseKey(BSTR *pVal)  {   CCPLWinSharedResource<CComBSTR>::GUARD guard(m_bsLicenseKey);    CComBSTR& bsLicenseKey = guard;    *pVal = bsLicenseKey.Copy();    return S_OK;  }  STDMETHODIMP CMySingleton::put_LicenseKey(BSTR newVal)  {   CCPLWinSharedResource<CComBSTR>::GUARD guard(m_bsLicenseKey);    CComBSTR& bsLicenseKey = guard;    bsLicenseKey = newVal;    return S_OK;  } 

Using this singleton class, one object (from one component) can set the license key property. Some other object (from a different component within the same process) can obtain the license key, as shown in the following code snippet:

 // One object sets the value  IMySingletonPtr spSingleton;  HRESULT hr = spSingleton.CreateInstance(__uuidof(MySingleton));  if (FAILED(hr)) {   _com_issue_error(hr);  }  spSingleton->LicenseKey = "MyMagicNumber1234";  ...  // Some other object gets the value  IMySingletonPtr spSingleton;  HRESULT hr = spSingleton.CreateInstance(__uuidof(MySingleton));  if (FAILED(hr)) {   _com_issue_error(hr);  }  ConstructFeatureListBasedOnLicenseKey(spSingleton->LicenseKey); 

Though COM singleton is a widely acceptable programming idiom, it has a few deficiencies and, in some cases, has a high potential of breaking the COM model [Box-99]. A better strategy is to separate the code from the shared state. In this case, the COM object itself is not a singleton. However, all the objects of the COM class share the same state.

graphics/01icon01.gif

Avoid implementing singletons in COM as much as possible. If you do have to implement one, be aware of the problems associated with it.


Shared Property Manager

Sharing properties among multiple objects in a thread-safe manner is such a common programming task that COM+ provides a COM class called the shared property manager (SPM) that lets multiple clients access shared data without the need of complex programming. The SPM model consists of the following three interfaces:

  • Interface ISharedProperty is used to set or retrieve the value of a property.

  • Interface ISharedPropertyGroup is used to group related properties together. This interface lets one create or access a shared property.

  • Interface ISharedPropertyGroupManager is used to create shared property groups and to obtain access to existing shared property groups.

The following code snippet demonstrates setting and retrieving a shared property using the SPM. The property is referred to as Key and is contained within the LicenseInfo group.

 #import "c:\winnt\system32\comsvcs.dll"   // Import the type                                            // library for the SPM  using namespace COMSVCSLib;  void CreateLicenseKeyPropertyAndSetItsValue()  {   // Instantiate the SPM    ISharedPropertyGroupManagerPtr spGroupMgr;    spGroupMgr.CreateInstance(     __uuidof(SharedPropertyGroupManager));    // Create a group called "LicenseInfo"    long lIsoMode = LockSetGet;   // lock each property individually    long lRelMode = Process;      // Do not destroy the group                                  // till the process quits    VARIANT_BOOL bExists;         // Return value to indicate if                                  // the group already exists    ISharedPropertyGroupPtr spGroup =      spGroupMgr->CreatePropertyGroup("LicenseInfo",      &lIsoMode, &lRelMode, &bExists);    // Create a property called "Key"    ISharedPropertyPtr spProperty =      spGroup->CreateProperty("Key", &bExists);    // Set the property value    spProperty->Value = "MyMagicNumber1234";  }  void ObtainLicenseKey()  {   // Instantiate the SPM    ISharedPropertyGroupManagerPtr spGroupMgr;    spGroupMgr.CreateInstance(     __uuidof(SharedPropertyGroupManager));    // Get the "LicenseInfo" group    ISharedPropertyGroupPtr spGroup =      spGroupMgr->GetGroup("LicenseInfo");    // Get the "Key" property    ISharedPropertyPtr spProperty = spGroup->GetProperty("Key");    // Use the property    ConstructFeatureListBasedOnLicenseKey (spProperty->Value);  } 

graphics/01icon01.gif

Shared properties can be shared only by the objects running within the same process. Therefore, if you want instances of different components to share properties, you have to install the components in the same COM+ application. Moreover, if the base client also wants to access the shared properties, the COM+ application has to be marked as the library application so that the base client as well as the components can run in the same process space.



< BACK  NEXT >


COM+ Programming. A Practical Guide Using Visual C++ and ATL
COM+ Programming. A Practical Guide Using Visual C++ and ATL
ISBN: 130886742
EAN: N/A
Year: 2000
Pages: 129

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