科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道基础软件COM高手总结的八个经验和教训

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

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

我为许多富于创造性的使用 COM 的工作方式感到惊讶。

作者:Jeff Prosise 来源:VC知识库 2007年10月19日

关键字: COM 经验 教训

  • 评论
  • 分享微博
  • 分享邮件
在日常工作中,我看到过许多由不同开发人员编写的 COM 代码。我为许多富于创造性的使用 COM 的工作方式感到惊讶,有一些使 COM 工作的巧妙代码可能连Microsoft 都没有想到。同样,看到一些错误一次又一次地重犯,使我免不了心灰意懒。这些错误很多都与线程和安全有关,完全不成比例,而这也正是 COM 文档资料中最缺少的两个领域。如果不仔细计划,它们也是最可能遇到的并可能会绊住您的两个领域。

  在下面的篇幅中,您将读到八位程序员的记述,这些教训都来自他们的痛苦经历。每个故事都是真实的,但为了保护无辜者,名字都已隐去。我的目的是,通过这些真实的 COM 故事,使您不再重蹈其他 COM 程序员的覆辙。它们还可能会帮助您在编写的代码中找出存在潜在问题的地方。无论情况如何,我想您都会获得愉快的阅读体验。

  总是调用 CoInitialize(Ex)

  几个月前,我收到了一封朋友的电子邮件,他就职于一家著名的硬件公司。他的公司编写了一个非常复杂的基于 COM 的应用程序,其中使用了许多进程内和本地(进程外)的 COM 组件。在开始时,应用程序创建了 COM 对象以服务于运行在多线程单元 (MTA) 中的各种客户端线程。该对象还可以托管给 MTA,这意味着接口指针可以在客户端线程之间自由交换。在测试中,我的朋友发现在应用程序准备关闭之前,一切都进行得不错。然后,不知是什么原因,对 Release 的调用(必须执行此调用,以便正确释放客户端占用的接口指针)被锁定了。他的问题是:“到底是哪里出了问题?”

  其实答案非常简单。应用程序的开发人员其他都做得很对,只有一点例外,而这点又非常重要:他们没有在所有的客户端线程中调用 CoInitialize 或 CoInitializeEx。现代 COM 的基本原则之一,就是每个使用 COM 的线程都应该先调用 CoInitialize 或 CoInitializeEx 来初始化 COM。这条原则是无法免除的。除了其他事情以外,CoInitialize(Ex) 应将线程放入单元中,并初始化重要的每线程状态信息(这对于 COM 的正确操作是必需的)。调用 CoInitialize(Ex) 失败通常会在应用程序生命期早期以失败的 COM API 函数的形式表现出来,最常见的是激活请求。但有时问题很隐蔽,直到一切都太晚了(例如对 Release 的调用一去不复返了)才表现出来。当开发小组将 CoInitialize(Ex) 调用添加到所有接触 COM 的线程之后,他们的问题就迎刃而解了。

  具有讽刺意义的是,Microsoft 竟是 COM 程序员有时不调用 CoInitialize(Ex) 的原因之一。Microsoft 知识库中包含的一些文档中说,调用 CoInitialize(Ex) 对基于 MTA 的线程来说不是必需的(有关示例,请参阅文章 Q150777)。是的,在很多情况下,我们可以跳过 CoInitialize(Ex) 而不会出现问题。但是,这样是不应该的,除非您知道自己在干什么,并且可以绝对肯定自己不会受到负面影响。调用 CoInitialize(Ex) 是没有害处的,因此我建议 COM 程序员始终从某个与 COM 相关的线程中调用它。

  不要在线程之间传递原始接口指针

  我咨询的首批 COM 项目之一就涉及到一个包含 100,000 行代码的分布式应用程序,该程序是由美国西海岸的一个大型软件公司编写的。该应用程序在多个机器上创建了数十个 COM 对象,并从客户端进程启动的背景线程中调用这些对象。开发小组遇到问题了,调用要么消失得无影无踪,要么在没有明显原因的情况下失败。他们给我演示的最惊人的症状是:当一个调用无法返回时,在同一台机器上启动其他支持 COM 的应用程序(包括 Microsoft Paint 等)会频繁导致这些应用程序被锁定。

  检查他们的代码后发现,他们违反了 COM 并发的一个基本规则,就是说,如果一个线程要与另一个线程共享一个接口指针,它应首先封送该接口指针。如果有必要,封送接口指针可使 COM 创建一个新的代理(以及一个新的信道对象,将代理和存根结对),以允许从另一个单元向外调用。不通过封送而将原始接口指针(内存中的一个 32 位地址)传递给另一个线程,会绕过 COM 的并发机制,并且如果发送和接收的线程位于不同的单元中,将出现各种不良行为。(在 Windows 2000 中,由于两个对象可以共享一个单元,但又位于不同的上下文中,因此如果线程位于同一个单元中,可能会使您陷入困境。)典型的症状包括调用失败和返回 RPC_E_WRONG_THREAD_ERROR。

   Windows NT 4.0 和更高版本可以使用一对名为 CoMarshalInterThreadInterfaceInStream 和 CoGetInterfaceAndReleaseStream 的 API 函数,在线程之间轻松地封送接口指针。假定您应用程序中的一个线程(线程 A)创建了一个 COM 对象,继而接收了一个 IFoo 接口指针,并且同一进程中的另一个线程(线程 B)想调用这个对象。在准备将接口指针传递给线程 B 时,线程 A 应该封送该接口指针,如下所示:

CoMarshalInterThreadInterfaceInStream (IID_IFoo, pFoo, &pStream);

  在 CoMarshalInterThreadInterfaceInStream 返回后,线程 B 就可以安全地取消封送该接口指针:

IFoo* pFoo;
CoGetInterfaceAndReleaseStream (pStream, IID_IFoo, (void**) &pFoo);

  在这些示例中,pFoo 是一个 IFoo 接口指针,pStream 是一个 IStream 接口指针。COM 在调用 CoMarshalInterThreadInterfaceInStream 时初始化 IStream 接口指针,然后在 CoGetInterfaceAndReleaseStream 内部使用和释放该接口指针。实际上,您通常要使用一个事件或其他同步化基元来协调这两个线程的行为 — 例如,让线程 B 知道接口指针已准备好,可以取消封送。

  请注意,以这种方式封送接口指针不会出现任何问题,因为 COM 有足够的智能,在不需要进行封送时不会去封送(或重新封送)指针。如果在线程之间传递接口指针时这样做,使用 COM 就轻松多了。

  如果调用 CoMarshalInterThreadInterfaceInStream 和 CoGetInterfaceAndReleaseStream 看起来太麻烦,您还可以通过将接口指针放在全局接口表 (GIT) 中,并让其他线程去那里检索它们,从而实现在线程之间传递接口指针。从 GIT 中检索到的接口指针在被检索时会自动封送。更多信息,请参阅 IGlobalInterfaceTable 中的文档。请注意,GIT 只存在于 Windows NT 4.0 Service Pack 4 及更高版本中。
    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章