`

COM高手总结的八个经验和教训之二

    博客分类:
  • COM
阅读更多

STA 线程需要消息循环 

  上一部分中描述的应用程序还有另一个致命缺陷。看看您是否能指出来。 

  这个特殊的应用程序恰好是用 MFC 编写的。在一开始,它使用了 MFC 的 AfxBeginThread 函数启动一系列辅助线程。每个辅助线程要么调用 CoInitialize 要么调用 AfxOleInit(MFC 中类似 CoInitialize 的函数)来初始化 COM。某些辅助线程则调用 CoCreateInstance 来创建 COM 对象,并将所返回的接口指针封送到其他辅助线程。从创建这些对象的线程中调用对象将非常顺利,但从其他线程的调用却从不返回。您知道这是为什么吗? 

  如果您认为问题与消息循环(或缺少消息循环)相关,那么答案完全正确。事实确实如此。当一个线程调用 CoInitialize 或 AfxOleInit 时,它是放在单线程单元 (STA) 中。当 COM 创建一个 STA 时,它会创建一个随附的隐藏窗口。以 STA 中的对象为目标的方法调用将转换为消息,并放入与该 STA 关联的窗口的消息队列中。当运行在该 STA 中的线程检索到代表方法调用的消息时,隐藏窗口的窗口过程就会将消息转换回方法调用。COM 使用 STA 执行调用序列化。STA 中的对象一次不可能接收一个以上的调用,因为每个调用都要传递给一个而且是惟一一个运行在对象单元中的线程。 

  如果基于 STA 的线程无法处理消息会怎么样呢?如果它没有消息循环又会怎么样呢?针对该 STA 中对象的单元间方法调用将不再返回;它们将在消息队列中被永远搁置。MFC 辅助线程中没有消息循环,因此如果寄宿在这些 STA 中的对象要从其他单元的客户端接收方法调用,那么 MFC 辅助线程和 STA 是配合不好的。 

  这个故事的寓意何在呢?STA 线程需要消息循环,除非您肯定它们不会包含要从其他线程调用的对象。消息循环可以像这样简单: 

MSG msg;
while (GetMessage (&msg, 0, 0, 0))
DispatchMessage (&msg);

  另一种方案是将 COM 线程移到 MTA 中(或者在 Windows 2000 中,移到中立线程单元,即 NTA 中),这里没有消息队列依赖项。 

  
单元模型对象必须保护共享数据 

  另一个困扰 COM 开发人员的通病是标记为 ThreadingModel=Apartment 的进程内对象。这项指定告诉 COM,对象的实例必须只能在 STA 中创建。它还可让 COM 自由地将这些对象实例放在任何主机进程的 STA 中。 

  假设客户端应用程序有五个 STA 线程,每个线程都使用 CoCreateInstance 来创建同一个对象的一个实例。如果线程是基于 STA 的,且对象标记为 ThreadingModel=Apartment,则这五个对象实例将在对象创建者的 STA 中创建。因为每个对象实例都在占用其 STA 的线程上运行,因此所有五个对象实例都可以并行运行。 

  到目前为止,一切良好。现在考虑一下,如果这些对象实例共享数据会发生什么情况。因为对象都在并发线程上执行,两个或更多的对象可能会同时尝试访问同一个数据。除非所有这些访问都是读取访问,否则就会酿成灾难。问题可能不会很快显现出来;它们会以和时间紧密相关的错误形式出现,因此很难诊断和重现。这就解释了以下事实的原因:ThreadingModel=Apartment 对象应该包括可同步对共享数据的访问的代码,除非您能够确定对象的客户端不会对执行访问的方法进行重叠调用。 

  问题在于,太多的 COM 开发人员相信 ThreadingModel=Apartment 能够使他们免于编写线程安全的代码。事实并非如此 ? 至少不完全如此。ThreadingModel=Apartment 并不意味着对象必须是完全线程安全的,它代表的是一个对 COM 的承诺,即访问两个或更多对象实例共享的数据(或此对象和其他对象的实例共享的数据)时是以线程安全的方式进行的。而提供该线程安全性的任务应该由您,即对象实现者来负责。共享数据的类型和大小多种多样,但大多是以全局变量、C++ 类中的静态成员变量和函数中声明的静态变量的形式出现。即使是以下这样无害的语句也会在 STA 中出问题:

static int nCallCount = 0;
nCallCount++;

  因为这个对象的所有实例都将共享一个 nCallCount 实例,编写这些语句的正确方式如下:

static int nCallCount = 0;
InterlockIncrement (&nCallCount);

  注意:您可以使用临界区、互锁函数或您希望的任何方式,但不要忘了访问基于 STA 的对象共享的数据时要进行同步化! 

  
谨慎启动用户 

  这里还有一个问题让许多 COM 开发人员都吃过苦头。去年春天,有一家公司向我紧急呼救,他们的开发人员使用 COM 构建了一个分布式应用程序,其中客户端进程运行在与远程服务器的 Singleton 对象相连接的网络工作站上。在测试过程中,他们遇到了一些非常奇怪的行为。在一种测试场景中,客户端对 CoCreateInstanceEx 的调用可使它们与 Singleton 对象正常连接。而在另一个场景中,对 CoCreateInstanceEx 的相同调用产生了多个对象实例和多个服务器进程,使客户端无法与同一个对象   实例连接,从而实际影响了应用程序。在这两个场景中,硬件和软件是完全相同的。 

  此问题似乎与安全有关。当处理远程激活请求的 COM 服务控制管理器 (SCM) 在另一台机器上启动一个进程时,它会为该进程分配一个标识。除非另外指定,它选择的标识就是启动用户的标识。换句话说,分配给服务器进程的标识与启动它的客户端进程的标识相同。在这种情况下,如果 Bob 登录机器 A,并使用 CoCreateInstanceEx 连接机器 B 上的 Singleton 对象,而 Alice 也在机器 C 上如法炮制,就会启动两个不同的服务器进程(至少在两台不同的 WinStation 上),实际上使客户端无法再用 Singleton 语义与共享的对象实例连接。 

  两个测试场景之所以会产生大相径庭的结果,其原因就是在一个场景(那个可以工作的场景)中,所有测试人员都使用只为测试而设置的一个特殊帐户以同一个人的身份登录。而在另一个场景中,测试人员都使用他们的普通用户帐户登录。当两个或更多的客户端进程具有相同标识时,它们可以成功连接到配置为假定启动用户标识的服务器进程。但是,如果客户端有不同的标识,SCM 会使用多个服务器进程(每个唯一客户端标识一个)分隔分配给不同对象实例的标识。 


图 1 DCOMCNFG 中的用户帐户

  找到问题以后,解决起来就很简单了:配置 COM 服务器,让其使用特定的用户帐户而不是假定启动用户的标识。完成这一任务的一种方式是在服务器机器上运行 DCOMCNFG(Microsoft 的 DCOM 配置工具),并将“launching user ”更改为“This user”(请参见图 1)。如果您喜欢通过编程方式进行更改(可能从安装程序着手),请在主机注册表的 HKEY_CLASSES_ROOT\AppID 部分的 COM 服务器项中添加 RunAs 值(请参见图 2)。


图 2 添加 RunAs 值到注册表中

  您还需要使用 LsaStorePrivateData 将 RunAs 帐户的密码存储为 LSA 密钥,并使用 LsaAddAccountRights 确保帐户拥有“Logon as batch job”的权限。(有关具体操作的示例,请参见 Platform SDK 中的 DCOMPERM 示例。请特别注意名为 SetRunAsPassword 和 SetAccountRights 的函数。)

使用线程或异步调用来避免 DCOM 超时设定太长 

  总是有人问我当 DCOM 无法完成远程实例化请求或方法调用时出现的超时设定太长的问题。典型的场景如下:客户端调用 CoCreateInstanceEx 来实例化远程机器上的一个对象,但是这台机器临时离线了。在 Windows NT 4.0 上,激活请求不会立即失败,DCOM 可能会花上一分钟或更长时间来返回失败的 HRESULT。DCOM 还可能花费很长时间,使指向已不再存在或其主机已离线的远程对象的方法调用失败。如果可能,开发人员应该如何避免这些较长的超时设定呢? 

  要回答这个问题,几句话是讲不清楚的。DCOM 高度依赖于基础网络协议和 RPC 子系统。并没有什么神奇的设置可让您限制 DCOM 超时设定的持续时间。但是,我经常使用两种技巧来避免较长超时设定的负作用。 

  在 Windows 2000 中,当调用在 COM 信道中挂起时,您可以使用异步方法调用来释放调用线程。(有关异步方法调用的介绍,请参 MSDN Magazine 2000 年 4 月刊的“Windows 2000: Asynchronous Method Calls Eliminate the Wait for COM Clients and Servers Alike”。如果异步调用在合理时间内没有返回,您可以通过调用用于初始化调用的调用对象上的 ICancelMethodCalls::Cancel 来取消它。 

   Windows NT 4.0 不支持异步方法调用,甚至在 Windows 2000 中也不支持异步激活请求。怎么解决呢?从背景线程调用远程对象(或是实例化该对象的请求)。使主线程在事件对象上阻塞,并指定超时设定值以反映您愿意等待的时间长度。当调用返回时,让背景线程来设置事件。假设主线程使用 WaitForSingleObject 阻塞,当 WaitForSingleObject 返回时,返回值可以告诉您,返回是因为方法调用或激活请求返回,还是因为您在 WaitForSingleObject 调用中指定的超时设定到期。您不能在 Windows NT 4.0 中取消挂起调用,但是至少主线程可以自由地执行自己的任务。 

  下面的代码演示了基于 Windows NT 4.0 的客户端如何才能从背景线程调用对象。 

//////////////////////////////////////////////////////
// Placing a Method Call from a Background Thread 
///////////////////////////////////////////////////// 
HANDLE g_hEvent;
IStream* g_pStream;

// Thread A
g_hEvent = CreateEvent (NULL, FALSE, FALSE, NULL);
CoMarshalInterThreadInterfaceInStream (IID_IFoo, pFoo, &g_pStream);
DWORD dwThreadID;
CreateThread (NULL, 0, ThreadFunc, NULL, 0, &dwThreadID);
DWORD dw = WaitForSingleObject (g_hEvent, 5000);
if (dw == WAIT_TIMEOUT) {
 // Call timed out
}
else {
 // Call completed
}
...

// Thread B
IFoo* pFoo;
CoGetInterfaceAndReleaseStream (g_pStream, IID_IFoo, (void**) &pFoo);
pFoo->Bar (); // Make the call!
SetEvent (g_hEvent);
CloseHandle (g_hEvent);

  在此示例中,线程 A 封送了一个 IFoo 接口指针,并启动线程 B。线程 B 取消封送了该接口指针,并调用 IFoo::Bar。无论调用返回所花费的时间有多长,线程 A 都不会阻塞超过 5 秒钟,因为它在 WaitForSingleObject 的第二个参数中传递的是 5,000 (单位为微秒)。这并不是太好的办法,但是如果“无论在线路的另一端发生什么情况,线程 A 都不会挂起”这一点很重要的话,忍受这种麻烦也算值得。

  共享对象并不容易 

  从我收到的邮件和在会议上被问到的问题判断,困扰许多 COM 程序员的一个问题是如何将两个或更多的客户端与一个对象实例连接。要回答这个问题,写出长篇大论(或是一本小册子)都很容易,但其实只要说明与现有对象的连接既不容易也不自动化,就足够了。COM 提供了大量创建对象的方式,包括很受欢迎的 CoCreateInstance(Ex) 函数。但是 COM 缺乏一种通用的命名服务,允许使用名称或 GUID 来标识对象实例。而且它没有提供内置的方式来创建对象,然后将它标识为调用的目标以检索接口指针。 

  这是不是意味着将多个客户端与单一对象实例连接就不可能了呢?当然不是。实现这一点有五种方式。在这些资源链接中,您可以找到更多信息甚至是示例代码,来指导您的操作。请注意,这些技术从一般意义上讲不能互换;通常,环境因素会决定哪种方式(如果有)适用于手边的任务: Singleton 对象 Singleton 对象就是只实例化一次的对象。可能会有 10 个客户端调用 CoCreateInstance 来“创建”Singleton 对象,但实际上,它们都是接收指向同一对象的接口指针。ATL COM 类可通过在其类的声明中添加 DECLARE_CLASSFACTORY_SINGLETON 语句,来转换为 Singleton。 

  文件名字对象 如果一个对象实现了 IpersistFile,并在运行中对象表 (ROT) 中使用文件名字对象(它封装了传递给对象的 IPersistFile::Load 方法的文件名称)注册了自己,那么客户端就可以使用文件名字对象连接对象的现有实例了。实际上,文件名字对象允许使用文件名称来命名对象实例,对象可在这些文件名称中存储它们的持久性数据。它们甚至能够跨机器工作。

  CoMarshalInterface 和 CoUnmarshalInterface 保存接口指针的 COM 客户端可以与其他客户端共享这些接口指针,只要它们愿意封送指针。COM 为愿意将接口指针封送给同一进程中其他线程的线程提供了优化(请参见教训 2),但是如果客户端线程属于其他进程,CoMarshalInterface 和 CoUnmarshalInterface 就是实现接口共享的关键途径了。

分享到:
评论

相关推荐

    2019年软件测试工程师个人工作总结范文.doc

    软件测试工程师个人工作总结范文中,作者从自己的经验出发,总结了软件测试工作中的一些重要知识点和经验教训。 首先,作者强调了尝试、努力和坚持的重要性,只有通过尝试、努力和坚持,才能获得成功的收获。作者...

    世界九大顶尖操盘高手的总结.doc

    【操盘高手的总结】 操盘高手们的经验和智慧是交易者宝贵的财富。他们通过长期实战,积累了丰富的交易策略和心理素质,以下是他们的一些核心观点: 1. **马蒂·舒华兹** 强调了资金管理的重要性。他认为,交易者在...

    软件工程专业毕业实习总结-大学生实习总结.docx

    在报告中,作者也分享了自己在实习期间的一些习惯和经验,如遇到问题先自己思索、不怕问高手帮忙、解决问题后要追根究底等。这些习惯和经验对软件工程专业的学生非常重要,可以帮助他们更好地完成实习和项目。 此外...

    要想成为SEO高手 必须具备5大基本特征

    同时,他们会将每次的经验教训进行总结,避免同样的问题再次出现,以此不断提升自己的优化能力。 5. **注重效率**:高效率是区分SEO高手和普通从业者的关键。高手们懂得如何在短时间内完成更多的工作,并且能找出...

    XX软件测试工作总结范文.doc

    XX软件测试工作总结范文 在软件测试领域,一个好...在软件测试领域,需要不断学习和实践,总结经验和教训,掌握更多的技术和技能,成长为高手。同时,需要与其他测试人员分享经验和教训,帮助新进人员快速入门和成长。

    编程高手谈编程

    总结起来,编程高手的经验教训包括:重视实践,选择合适的编程语言和书籍,快速解决问题,理解操作系统和硬件,注重程序的容错性和风格,以及不断培养创新思维。这些都是成长为一名优秀程序员的必经之路。

    ERP沙盘模拟大赛高手整理经验集.doc

    本文将总结ERP沙盘模拟大赛的经验和教训,帮助未来的选手更好地准备和应对比赛。 首先,企业资源规划是企业管理的核心,包括业务决策、运营管理、财务管理等方面。在ERP沙盘模拟大赛中,我们需要从战略的角度来看待...

    数学中国-美赛经验总结.pdf

    - 团队中通常包含数学建模专家、编程高手和论文撰写能手。 #### 角色分工: - 明确每个成员的任务和责任,比如谁负责模型构建、谁负责编程实现等。 - 保持良好的沟通机制,确保团队内部信息流通顺畅。 ### 5. 时间...

    CSDN开发高手03-10

    根据提供的信息,我们可以总结并深入探讨以下几个IT领域的知识点: ### Windows 开发 Windows 平台下的软件开发一直是程序员关注的重点之一。随着技术的发展,Windows 开发不仅限于传统的桌面应用程序,还包括了...

    完整word版-单片机课程设计总结.doc

    单片机课程设计总结 在这篇课程设计总结中,我们可以看到作者对单片机课程设计的总结和心得体会。作者首先提到,理论知识如果不与...同时,作者也强调了团队协作和学习基础的重要性,为我们提供了有价值的经验和教训。

    02574 业务培训--沟通中,怎样成为说话高手.doc

    - 遂事不谏,即对于正在进行的事情,不要轻易劝阻,让事情自然发展,然后在事后总结经验教训。 - 既往不咎,是指对于已经发生的事情,不要过分追究责任,以免伤害他人积极性。 2. **不同事情,不同说法**: - ...

    [建筑结算书]建筑结算见习总结.pdf

    在建筑工程领域,结算是一项至关重要的工作,涉及到成本控制和经济效益的...面对新的挑战,见习期的宝贵经验和教训将成为应对未来工作的重要支撑。在这个过程中,持续学习和总结是不断提升自我,实现专业成长的关键。

    软件实习总结3篇.doc

    2. 工作方法:在编写代码时,实习生需要不断学习新知识,如通过网络资源自我提升,并将遇到的问题和解决方案记录下来。在团队中,与开发高手交流也是提升技能的重要途径。保持学习的热情和毅力,即使面临压力,也要...

    大学生营销总结.docx

    【大学生营销总结】 在大学生营销的实践中,销售心得体会至关重要。首先,销售不仅仅是比较产品的价格和配置,而是要强调产品的价值。...这些经验教训对于任何阶段的销售人员来说都具有宝贵的指导意义。

    项目管理学习笔记(案例分析

    案例分析是指通过对实际项目的分析和研究,总结出项目管理的经验和教训,并将其应用于未来的项目管理中。案例分析可以帮助项目经理和团队成员更好地理解项目管理的原理和方法,从而提高项目的成功率。 3. 项目管理...

    高质量C编程高手整理版

    - **一些心得体会**(第9.9节):总结类构造、析构过程中的经验和教训。 #### 九、类的继承与组合 **10. 类的继承与组合**(第10章) - **继承**(第10.1节):解释类的继承机制及其优点。 - **组合**(第10.2节...

    数学建模个人经验谈.pdf

    #### 八、总结 数学建模是一项集思广益、团队合作的活动。通过精心准备、合理分工和不断学习,可以有效地提升团队的整体实力。希望以上经验和建议能够帮助更多的人在数学建模竞赛中取得优异的成绩。 ### 结语 ...

    《DB2中国》特刊电子杂志

    自1989年发布以来,DB2一直是数据库领域的领军者之一,历经25年的发展历程,它在数据管理领域积累了丰富的经验和技术创新。 - **DB2在中国的影响**: 在中国,DB2同样拥有广泛的用户基础和技术支持社区。《DB2中国》...

Global site tag (gtag.js) - Google Analytics