求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
COM组件间调用的性能问题
 

作者:山姆 ,发布于2013-1-31,来源:博客园

 

多线程编程是大家都比较头疼的问题,不小心就会碰到死锁,野指针,同步调用问题等等,虽然在客户端编程方面会带来不少好的体验,比如界面和处理在不同的线程,则不会卡住界面,但是相对于他的副作用来说,让不少人还是望而却步。

QQ 客户端就是这样一个例子,从QQ重构的3个大版本来说,也一直在回避这个问题。Hummer在设计的时候为了防止编程的复杂性和后期的难以维护,也主动放弃了多线程特性(部分底层,socket等会有多线程),至少模块间调用是不会有多线程的,另外QQ是基于COM的组件编程,所有模块都是COM封装,有复杂的COM调用和引用计数问题,另外QQ的所有默认逻辑都是基于同步调用的,如果引入多线程,则很多地方都要处理异步信息,问题则会更多,开发难度则会更大。

真的没有办法在COM间调用使用多线程吗?当然这里说的是安全的使用,假如你要暴力的传递接口指针给别的线程,出现调用问题,这里是无法控制的,尤其是QQ的Service接口,大量的引用计数在操作,如果不加锁,势必会引发随机crash(有历史前车之鉴)。

【从套间说起】

QQ的COM组件自然使用的是STA单线程套间, 如果想使用多线程很自然的想法就是把COM组件创建在另外一个单独的线程,也就是另外一个独立套间,这样主线程的调用逻辑可以转到这个独立的线程COM中进行操作。具体实施上有两个方法:

1.把独立线程创建的COM接口直接给到主线程,让主线程去调用。

2.使用列集散集进行接口调用。

第一种方法就是上面说的暴力调用,因为QQ的组件不支持多线程,引用计数没有被保护,野蛮调用会带来很多问题。除非真正支持多线程,加入多线程保护,这个工作量和复杂性相当大,明显行不通。

第二种就是这里要讲的的方法。

STA 单线程套间对象的调用,如果是被其他线程调用,一定是需要进行列集散集的,COM库会保证调用的同步和安全性。

列集散集有两种方法,一种是自定义列集散集,一种是COM库实现的标准的列集散集,自定义列集散集实现起来很复杂,这里也不多说,一般如果没有特殊的类型(自定义接口不算做特殊类型),使用标准的列集散集也应该是足够了,如果使用vs创建工程,会默认有个*PS工程(Proxy and Stub),只需要编译这个工程就会自动帮你列集散集你的接口,前提你需要注册你的*Ps.dll到系统(这里后面会说)。

期间用到了两个很有用的API:

1.在你创建COM组件的线程使用CoMarshalInterThreadInterfaceInStream函数,将接口列集到Stream对象中。

2.在你的主线程,或者需要调用接口的地方使用CoGetInterfaceAndReleaseStream来获取接口的代理对象。

这样就可以安全的在两个线程间进行COM调用了。

有的说只有程外组件才会用到代理存根,其实只要是跨套间调用,都会用到代理存根。

【问题解决了吗】

从调用来看,貌似是执行方在另外一个线程执行代码了,但是卡住界面的问题还没有解决,为什么呢?因为这种调用,其实还是同步调用的(也是我们想要的),也就是说从调用接口的线程来看,堆栈还是阻塞在接口调用的地方,并没有往下执行(如果真的往下执行,还真的有问题),而真正执行的代码在另外一个线程,我们的工作白做了?

仔细想一想这里和之前同一个线程中调用接口的区别:

1.在同一个线程调用,就是代码同步执行,COM组件执行多久,界面会卡住多久。

2.使用跨线程调用,虽然代码阻塞在调用的地方,但是我们知道,其实是COM创建了一个消息循环在等待另外一个线程调用的结束,界面没有响应是因为COM库没有将捕获的消息抛出来。

这时候另外一个有用的接口就显现出来了:IMessageFilter,这个接口就是让你有机会处理COM接口间调用的各种消息的,其中有个函数MessagePending,MSDN的解释:

A client-based method called by COM when a Windows message appears in a COM application’s message queue while the application is waiting for a reply to a remote call.

那么需要我们做的就是注册自己的IMessageFilter接口,在MessagePending中处理各种界面消息,其实就是我们经常在主线程中使用的消息泵:PeekAndPump,这样所有的消息都会被正常处理。

这里还有一个问题就是如果界面处理了一些消息,自然要考虑到界面调用重入的一些场景,比如在Pump消息的时候只需要将和界面响应的消息抛出即可,这里和讨论无关,就不展开了。

【还漏了一点】

前面说到代理存根dll必须注册到系统注册表(regsvr32 %1)才能使用,这里有些不太完美的地方,我们希望的是用户将目录文件放在任何地方都不影响使用的绿色版,否则用户换个目录,就无法使用了,这肯定不是我们期望的,尤其Vista系统以后,注册到系统成本很高,需要弹UAC,需要用户确认。

在没有注册的有什么办法让系统可以正常加载我们的代理存根dll呢?

在解决这个问题之前,我们需要先搞清楚系统是如何找到这个dll并加载的。

首先我们打开Windbg的Event Filters ,将Load module 打开,这样当有模块被load起来的时候就会自动中断下来。

F5,当中断在我们的ps.dll 被加载的时候,我们看到的堆栈如下:

我们看到的是ole在另外一个线程创建了一个标准的列集散集对象,然后这个对象在创建存根对象的时候进行了加载我们dll的动作。

我们看到了这里使用的是CoGetClassObject函数,我们知道CoGetClassObject是从系统RTO表中查询对象,如果不存在,然后才会从注册表中创建对象。

我们仔细看下调用CoGetClassObject的一些参数:

发现获取的并不是标准的COM IClassFactory接口,而是IPSFactoryBuffer接口。

这个接口就相当于代理存根对象的创建工厂接口,这里和标准的创建COM组件方式不同。

但是 CLSID是CLSID_PSOlePrx32,这个只是我们ps的CLSID对象的一个别名。具体不同的代理存根对象,这个clsid是不同的,通过查看注册表或者使用OleView不难获知这个CLSID,其实在vs生成的代码中,这个clsid是通过复用我们的一个接口id来生成的。

接下来操作就很明显了,我们自己Load dll,创建我们的PS对象,然后使用CoRegisterClassObject函数将对象注册到ROT表中,这样当系统创建对象的时候就会拿到我们注册的对象。当然还需要使用CoRegisterPSClsid将你声明的所有接口都注册在内存中。否则你获取接口的时候会返回0×80040155(没有注册接口).

【结束语】

至此,重新体验一下,所有界面已经完全不卡了,即时COM的操作耗时很长,界面也可以正常操作,这个方法只是解决调用一些逻辑会卡住界面的性能问题,对于如果需要尽快返回,或者异步执行,则需要另外的方式解决。

如果需要从架构方面解决类似的调用问题,是很好的方案,当然我们需要重新设计一下我们的线程如何创建,对象如何创建,哪些对象应该放在子线程等等问题都需要一并考虑并管理起来,Hummer 现在如果做这么多重构,需要很多人力和成本,中间可能还有很多不确定性,如果有机会其他小软件可以先尝试起来。

当然这只是一个开始,QQ做为一个超大规模客户端软件,性能问题不可小觑,我们是希望透过QQ从架构层面去解决各种问题。


 
分享到
 
 


专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...