管理无状态事务性 COM+ 组件状态的简单解决方案






4.89/5 (8投票s)
用于保留无状态组件状态的可重用面向对象框架。
摘要
构建能够并发管理大量客户端请求的多层分布式系统一直是COM开发者的挑战。
COM+引入了“每个客户端一个对象”的理念,以及其JITA扩展——“每个客户端一个方法”模型。该框架提供了一个高效的运行时环境,用于实现极具可伸缩性的企业解决方案。这些机制背后的主要思想是提出一种实现线程安全类的架构,允许并发访问其方法和属性,同时提供性能和可伸缩性要求所需的并发方法调用手段。为了简化分布式系统的开发,COM+运行时环境提供了一个包含一系列服务的框架。在我看来,其中最重要的无疑是事务管理服务。理论上,事务被定义为代表特定客户端发起者完成的工作的单一实体。另一方面,利用声明式事务不可避免地需要使用COM+的JITA——“即时激活”功能[EWA-2001]。这项服务与对象生命周期管理有关。使用JITA功能的对象的选项是,在客户端实际释放它之前,从其存根分离并从其上下文中释放。当客户端下次调用该对象的方法时,COM+环境会连接到一个新的实例到存根上。
问题
如果您正在实现可伸缩的服务器端COM+组件,旨在管理相对大量的客户端,那么您很可能会使其无状态。问题在于,这些系统通常需要维护客户端特定的信息,这些信息必须在连续的COM+对象方法调用之间保留。
解决方案
COM+提供了多种在连续客户端请求(方法调用)之间保留状态的技术。我将介绍的解决方案基于COM+的内部架构,并演示了如何设计和实现复杂的无状态事务性组件,这些组件可以轻松维护客户端特定的信息。深入了解COM+上下文内部,我们可以看到它实际上只是一个定义明确的可执行文件区域。每个上下文负责为其中执行的对象提供一组预定义的特定服务。每个COM+对象只有一个上下文。COM+负责上下文的生成以及对象和上下文之间的关联。该框架确保对象在适当的上下文中运行,并且如果两个对象具有相同的服务需求,它们将被关联到同一个上下文。然而,大多数配置类的默认属性设置方式是,每个新的COM对象实例都必须放置在一个新的、独立的上下文中。此外,如果COM组件支持声明式事务和JITA服务,那么COM+会强制对象独立存在于上下文中[EWA-2001]。启用EventTrackingEnabled
属性(控制COM+统计功能)需要每个新的COM+对象实例都与实例专用的上下文相关联[EWA-2001]。对连续方法调用的详细分析表明,如果满足上述条件,COM+会映射一个唯一的对象存根和COM+上下文ID对。客户端代码中的每个代理指针都“关联”到一个唯一的上下文ID。因此,我们可以通过将其映射到上下文ID来在服务器端保留客户端特定的信息。设计和实现
JITA服务允许对象在客户端释放它之前自行停用。COM+拦截层在每次COM调用结束时检查“完成”位。因此,使用IObjectContext::SetComplete()
或IObjectContext::SetAbort()
设置“完成”位很重要,以便通知COM+环境对象在每次方法调用结束后必须停用。如果类支持事务并且已启动DTC事务,设置“完成”位可能非常关键。它实际上决定了分布式事务的最终结果。我们可以通过将AutoComplete
属性设置为TRUE
来覆盖默认行为,强制对象在调用完成时自动停用。每次客户端进行方法调用并调用IObjectControl::Activate()
时,都会使用IObjectContextInfo::GetContextId()
检索上下文ID。此ID将用作在映射容器中存储和查找客户端特定信息的键。一个保留事务性COM+对象状态的COM+组件可以存储信息在一个全局线程安全的STL映射中,以实现更好的性能和效率。我们需要考虑的一个重要事项是,关联容器应设计成线程安全的,以便提供对其维护信息的正确访问。下面是基类CSafeMap模板类的接口。
template <class BOOL bThreadSafe> class CSafeMap: private map<tstring, T*, CNocaseCmp> { // // ... Other details ignored for the sake of simplicity... // public: // // An efficient "add or update" method // Returns: TRUE if pObject has been added to the map // FALSE if pObject has replaced existing one // BOOL AddOrUpdate( TCHAR* pszKey, // Key T* pObject // Pointer to a valid T object ); // // Remove an element from the container // void Remove(TCHAR* pszKey); // // Returns a copy of an element by its key // BOOL GetObjectCopyByKey(TCHAR* pszKey, T& copyObject); };
为了最小化耦合并避免在CSafeMap
中过度使用继承,我采用了私有继承。非公共继承表达了“IS-IMPLEMENTED-IN-TERMS-OF”(“以...实现”)。尽管CSafeMap
没有重写任何映射的虚拟函数,但为了简洁起见,我还是决定使用“IS-IMPLEMENTED-IN-TERMS-OF”而不是“HAS-A”(即组合),后者在考虑CSafeMap
的所有功能需求时是首选。更多细节请参阅[SUT-2000]第24项。
实际上CSafeMap
是一个简单的关联映射容器,使用上下文ID作为键,对象指针(例如CCompObjectState
)作为对应的值。关联容器要求其元素可以排序。默认情况下,使用"std::less"
运算符来定义顺序。为了改变默认比较,我设计了CNocaseCmp
类。它实现了bool operator()(const tstring& x, const tstring& y)
运算符,用于不区分大小写的字符串比较。CSafeMap
实现中更值得关注的是AddOrUpdate()
方法。它基于Scott Meyers的完美实现。简而言之,为了实现更好的性能,该方法使用map::insert()
方法来向映射添加和更新元素。更多细节请参阅[MEY-2001]第24项。下面是AddOrUpdate()
实现部分代码片段。
// // An efficient "add or update" method // Returns: TRUE if pObject has been added to the map // FALSE if pObject has replaced existing one // BOOL AddOrUpdate( TCHAR* pszKey, // Key T* pObject // Pointer to a valid T object ) { CLockMgr<CCSWrapper> lockMgr(m_Mutex, bThreadSafe); BOOL bResult; // Find where pszKey is or should be CSafeMap::iterator lb = lower_bound(pszKey); // if lb points to a pair whose key is equivalent to the pszKey if ( ( lb != end() ) && !( key_comp()(pszKey, lb->first) ) ) { if ( m_bOwnsObjects ) { T* pLastObj = lb->second; // // If the map owns all objects we should release // the this instance in order to prevent memory leaks // delete pLastObj; } // // Updates the pair's value // lb->second = pObject; // // replaces existing one // bResult = FALSE; } else { // // when an "add" is performed, insert() is more efficient // than operator[]. // For more details see -item 24 page 109 "Effective STL" by Meyers // // Adds pair(pszKey, pObject) to the map insert( lb, value_type(pszKey, pObject) ); // // added to the map // bResult = TRUE; } return bResult; }
CSafeMap
的另一个重要特性是,它通过设置m_bOwnsObjects
属性来提供拥有存储对象(owning stored objects)的选项。这使得CSafeMap
可以在对象从映射中移除或映射被销毁时释放对象。默认情况下,m_bOwnsObjects
的值设置为TRUE
。然而,可以使用Set_OwnsObjects()
访问器方法来更改它。
当您使用参数bThreadSafe = TRUE
实例化CSafeMap
时,它是一个线程安全的类,也就是说——它被设计成允许多个线程访问它,从中存储和获取信息。容器类聚合了一个CCSWrapper
的实例,这是一个简单的CRITICAL_SECTION
包装器。但是CSafeMap
的实现并没有直接使用CCSWrapper
——而是通过模板类CLockMgr
来实现。CSafeMap
的实现不是直接调用Enter()
和Leave()
方法——而是将一个CLockMgr
实例化在栈上,因此,当CSafeMap
的线程安全方法进入时,CLockMgr
的构造函数会调用CCSWrapper::Enter()
来获取锁,当方法离开时,CLockMgr
超出作用域,这会强制调用其析构函数,在析构函数中释放CCSWrapper
的锁。这种方法的优点是我们不必担心在CSafeMap
的方法中抛出异常时需要释放锁的异常处理。
最后但同样重要的是,我实现了一个名为CStateMgr
的类,它继承自CSafeMap
,并使用CCompObjectState
作为模板参数。该类封装了单例模式的实现,并遵循“IS-A”CSafeMap
类的模型。CStateMgr
的实现非常直接,结合了Andrei Alexandrescu在“Modern C++ Design”中解释的“双重检查锁定模式”和Meyers在“More Effective C++”第26项中描述的单例模式。下面是GetInstance()
静态方法的实现。
CStateMgr& CStateMgr::GetInstance() { if (!sm_pInstance) { CLockMgr<CCSWrapper> guard(g_SingeltonLock, TRUE); if (!sm_pInstance) { static CStateMgr instance; sm_pInstance = &instance; } } // if return *sm_pInstance; }
为了完善整个图景,我们应该在COM对象CTrickyObject
中声明两个额外的接口方法——AssignState()
和RemoveState()
。它们的作用是注册和注销对特定对象保持状态的兴趣。我将它们定义在一个单独的接口ICasheState
中,但实现者可以决定它们是否应该成为同一COM对象实现的任何其他COM接口的一部分。只要实现提供了两个用于分配状态和释放已分配资源的方法,它就可以工作。下面是COM类接口的片段。
class ATL_NO_VTABLE CTrickyObject: public CComObjectRootEx<...>, public CComCoClass<CTrickyObject...>, public IDispatchImpl<ITrickyObject...>, public IObjectControl, public ICasheState { // // Other details ignored for the sake of simplicity // // // ICasheState // public: // // Assigns state information // STDMETHOD(AssignState)(/*[in]*/ BSTR bstrParam); // // Releases resources used for maintaining state information // STDMETHOD(RemoveState)(); };
这是AssignState()
方法的实现。请注意,如果m_guidContextId
未初始化(即为GUID_NULL
)——则此方法返回的结果代码为CO_E_NOT_SUPPORTED
。
STDMETHODIMP CTrickyObject::AssignState(BSTR bstrParam) { HRESULT hr = S_OK; // // Get the only one instance of the state manager // CStateMgr& stateMgr = CStateMgr::GetInstance(); if (m_guidContextId != GUID_NULL) { _bstr_t bstrProperty(bstrParam); CCompObjectState* pState = new CCompObjectState(); // // Let's keep the data we would like to preserve // pState->Set_Param( (TCHAR*)bstrProperty ); // // Adds/updates an element to the map using context ID // as a key // stateMgr.AddOrUpdate( _bstr_t( CComBSTR( m_guidContextId ) ), pState ); } // if else { // // The operation attempted is not supported. // hr = CO_E_NOT_SUPPORTED; } return hr; }
最后,我们可以看看客户端。它是一个简单的控制台应用程序,实现了两个演示函数——StatelessCalls()
和StatefulCalls()
。StatelessCalls()
演示了常规的COM+对象操作。StatefulCalls()
使用了提供的机制来存储每个COM+对象的信息。它展示了组件如何在连续的COM+方法调用之间维护信息。
使用VC++ 6.0编译和调试TrickyComp.DLL
- 确保启用“异常处理”,使用VC RTL的多线程调试版本。
- 如果想调试,请将活动配置设置为“Debug”。
- 设置“调试会话的可执行文件”。
- 编译TrickyComp.DLL
- 编译客户端项目
- 使用Component Services MMS管理单元注册TrickyComp.DLL。
使用提供的框架创建新的COM+组件
- 创建一个新的ATL COM项目,并选择支持MTS的DLL。
- 将以下文件添加到您的项目中
- Common.h
- CompObjectState.h
- CompObjectState.cpp
- LockMgr.h
- LockMgr.cpp
- StateMgr.h
- StateMgr.cpp
- SafeMap.h
- 在您的COM对象中声明
m_guidContextId
属性。它将保留在IObjectControl::Activate()
实现中通过IObjectContextInfo::GetContextId
检索到的上下文ID的值。确保在Activate()
方法的开头将其设置为GUID_NULL
。 - 声明并实现
AssignState()
和RemoveState()
方法。更多细节请参阅示例代码。
摘要
在无状态环境中管理状态可以通过多种不同的方法来处理。所提出的机制非常高效,并且适用于各种场景。然而,您应该非常仔细地考虑,分析确切的系统需求,然后决定您是否真的需要将事务性COM+组件转变为能够维护客户端特定信息的组件。
参考文献
[EWA-2001] Tim Ewald "Transactional COM+", 2001
[SUT-2000] Herb Sutter "Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions", 2000
[MEY-2001] Scott Meyers "Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library" 2001
[ALE-2001] Andrei Alexandrescu "Modern C++ Design: Generic Programming and Design Patterns Applied"
[MEY-1995] Scott Meyers "More Effective C++: 35 New Ways to Improve Your Programs and Designs"
[TAP-2000] Pradeep Tapadiya "COM+ Programming: A Practical Guide Using Visual C++ and ATL"
Platform SDK, COM+ Component Services