- 浏览: 2031413 次
- 性别:
- 来自: 北京
文章分类
- 全部博客 (651)
- ACE (35)
- BAT (9)
- C/C++ (116)
- fast-cgi (14)
- COM (27)
- python (59)
- CGI (4)
- C# (2)
- VC (84)
- DataBase (29)
- Linux (96)
- P2P (6)
- PHP (15)
- Web (6)
- Memcached (7)
- IME输入法 (11)
- 设计模式 (2)
- 搜索引擎 (1)
- 个人情感 (4)
- 笔试/面试 (3)
- 一亩三分地 (33)
- 历史 (2)
- 地理 (1)
- 人物 (3)
- 经济 (0)
- 不仅仅是笑哦 (43)
- 小故事大道理 (2)
- http://www.bjdsmyysjk120.com/ (0)
- http://www.bjdsmyy120.com/ (0)
- 它山之石可以攻玉 (15)
- 大学生你关注些什么 (28)
- 数据恢复 (1)
最新评论
-
luokaichuang:
这个规范里还是没有让我明白当浏览器上传文件时,STDIN的消息 ...
FastCGI规范 -
effort_fan:
好文章!学习了,谢谢分享!
com技术简介 -
vcell:
有错误os.walk(strPath)返回的已经是全部的文件和 ...
通过python获取目录的大小 -
feifeigd:
feifeigd 写道注意:文章中的CPP示例第二行 #inc ...
ATL入门:利用ATL编写简单的COM组件 -
feifeigd:
注意:文章中的CPP示例第二行 #include " ...
ATL入门:利用ATL编写简单的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 及更高版本中。 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 的函数。)
发表评论
-
如何使用BHO定制你的Internet Explorer浏览器
2009-08-20 11:26 2156如何使用BHO定制你的In ... -
定制IE浏览器的尖兵利器 - BHO
2009-08-19 18:28 2851作者:peterzb(个人 ... -
ATL入门:利用ATL编写简单的COM组件
2009-08-19 18:26 17749使用ATL编写一个简单的COM服务器文/赵湘宁 ... -
用 ATL ActiveX 绘制任意平面函数的曲线
2009-08-19 18:23 1645用 ATL ActiveX 绘制任意 ... -
COM多线程原理与应用
2009-08-19 18:14 2696COM多线程原理与应用 目录: COM多线程原 ... -
com技术简介
2009-07-28 11:32 2719一、COM是一个更好的C++1、COM 是什么Don Box ... -
COM高手总结的八个经验和教训之二
2009-07-28 11:31 2013STA 线程需要消息循环 ... -
COM基础知识
2009-07-28 11:30 1910(1) COM组件实际上是一个C++类,而接口都是纯虚类。 ... -
COM原理及应用----概述
2009-07-28 11:29 18501、组件设计的原始目的 跨平台、跨网络、积木式搭建程序 2、组 ... -
COM原理及应用----COM对象和接口
2009-07-28 11:29 44171、COM对象的理解 ... -
COM原理与应用----COM的实现
2009-07-28 11:28 29551、COM的实现与操作系 ... -
COM原理及应用----COM特性
2009-07-28 11:28 20941、面向对象系统的三个最基本的特性 封装性、多态性 ... -
COM原理及应用----用Visual C++开发COM应用
2009-07-28 11:27 36401、MFC和ATL 对于COM应用的开发来说,建 ... -
COM原理及应用----可连接对象
2009-07-28 11:26 23501、COM的高级特性 COM规范中有一些高 ... -
COM原理及应用---- 结构化存储
2009-07-28 11:25 24451、结构化存储 ... -
COM原理及应用----命名和绑定技术
2009-07-28 11:25 32821、COM对象的创建方法 客户程序可以通过 ... -
COM原理及应用----统一数据传输
2009-07-28 11:24 24551、概述 COM提供了应用之间数据交换的标 ... -
COM原理及应用----分布式COM(DCOM)
2009-07-28 11:24 30821、DCOM COM的 ... -
COM原理及应用----自动化(Automation)对象
2009-07-28 11:23 22931、自动化技术 自动化技术既以前提到的OL ... -
COM原理及应用----ActiveX控件
2009-07-28 11:22 41081、ActiveX控制 ActiveX控制 ...
相关推荐
SEO(搜索引擎优化)是互联网营销领域中的核心技能之一,它涉及到如何通过技术手段和策略来提升网站在搜索引擎结果页面的排名。要成为SEO高手,具备以下五个基本特征至关重要: 1. **专注**:真正的SEO专家会对这个...
根据提供的信息,我们可以总结并深入探讨以下几个IT领域的知识点: ### Windows 开发 Windows 平台下的软件开发一直是程序员关注的重点之一。随着技术的发展,Windows 开发不仅限于传统的桌面应用程序,还包括了...
- 团队中通常包含数学建模专家、编程高手和论文撰写能手。 #### 角色分工: - 明确每个成员的任务和责任,比如谁负责模型构建、谁负责编程实现等。 - 保持良好的沟通机制,确保团队内部信息流通顺畅。 ### 5. 时间...
在设计之前,作者做了相当丰富的准备,首先巩固一下课程理论,再一遍熟悉课程知识的构架,然后结合加以理论分析、总结,有了一个清晰的思路和一个完整的软件流程图之后才着手设计。在设计程序时,作者了解到,反复...
在这个高度合作的行业中,无论个体能力多么出色,团队协作始终是推动项目成功的关键因素之一。正如Linux之父Linus Torvalds所展示的那样,即使是顶级天才也需要借助强大的团队力量来创造奇迹。在全球范围内,为Linux...
每次失败都是宝贵的经验,从中总结教训,找到问题所在,然后再继续前进。 #### 16. 反思与总结 定期对自己所学的知识进行反思和总结,可以帮助你更清晰地认识到自己的不足之处,以及下一步需要努力的方向。可以通过...
- **经验总结**:每场比赛结束后都应该进行复盘,分析比赛中的表现,总结经验教训,并学习其他高分选手的解题思路和编程技巧。 - **心态调整**:保持冷静的心态对于比赛至关重要。面对难题时不应过分焦虑,而应将...
在数学建模的实践中,算法的选择和应用是关键环节之一。例如,对于图上两点间最短路径问题,Floyd算法是一种常见的选择。Floyd算法是一种动态规划算法,用于在加权图中找到两个顶点之间所有可能路径的最短路径。该...
- **工作总结**:定期撰写工作总结报告,总结过去一段时间内的工作成绩和经验教训,为未来的工作提供参考。 - **自我反思**:在总结工作中不仅要强调成绩,也要勇于面对存在的问题和不足之处,并提出改进措施。 综...
- **回顾与改进**:鼓励团队定期进行回顾会议,总结经验教训,并不断寻找改进的机会。 - **持续学习与适应变化**:提倡一种持续学习的文化,以便能够快速适应市场和技术的变化。 #### 四、案例研究与实践经验 - **...
5. **反思总结**:分析解题过程,总结经验教训,以便在未来遇到类似问题时能更快地找到解决办法。 这个压缩包内的试题资源,对于准备蓝桥杯或其他编程竞赛的学生来说,是一份不可多得的参考资料。通过反复练习和...
这些宝贵的经验和教训对日后的学习和工作产生了积极的推动作用。 总之,蓝桥杯大赛不仅是一项具有广泛影响力的全国性 IT 学科赛事,也是一个促进学生全面发展、提升自身竞争力的宝贵平台。对于参赛者来说,不仅要...
- **积极思考创造积极的人生,消极思考导致消极的人生**:积极乐观的心态是创业成功的关键因素之一。积极思考能够激发创造力,帮助创业者找到解决问题的方法。 - **成功的人是跟别人学习经验,失败的人只跟自己学习...
自1989年发布以来,DB2一直是数据库领域的领军者之一,历经25年的发展历程,它在数据管理领域积累了丰富的经验和技术创新。 - **DB2在中国的影响**: 在中国,DB2同样拥有广泛的用户基础和技术支持社区。《DB2中国》...
- **技术专家**:这类程序员专注于某一特定技术领域,拥有深厚的技术积累和经验。 - **全能型程序员**:能够掌握多种技术栈,灵活应对不同项目需求的程序员。 - **创业型程序员**:他们不仅仅是技术高手,还具备商业...
- **徐子彬**在《数学建模竞赛后的思考》一文中,从个人角度出发,反思了参赛过程中的得失,总结了宝贵的经验教训。他强调了团队合作的重要性,以及如何有效地分配任务、沟通协作等问题。 ### 必备技能 #### 数学...
《一件()事》的写作指导主要集中在如何有效地记录并表达生活中的事件,使之生动有趣、意义深远。以下是一些关键的写作要点: 1. **完整的故事结构**:一个优秀的记事作文应该具备清晰的开头、发展和结尾。开头部分...