关于COM及套间(Apartment)知识

什么是COM组件?
   COM组件是以WIN32动态链接库(DLL)或可执行文件(EXE)形式发布的可执行代码组成。
   COM组件是遵循COM规范编写的   COM组件是一些小的二进制可执行文件
   COM组件可以给应用程序、操作系统以及其他组件提供服务
   自定义的COM组件可以在运行时刻同其他组件连接起来构成某个应用程序
   COM组件可以动态的插入或卸出应用
   COM组件必须是动态链接的
   COM组件必须隐藏(封装)其内部实现细节 
   COM组件必须将其实现的语言隐藏
   COM组件必须以二进制的形式发布
   COM组件必须可以在不妨碍已有用户的情况下被升级
   COM组件可以透明的在网络上被重新分配位置
   COM组件按照一种标准的方式来宣布它们的存在 
套间:
套间的提出是为了组件在多线程环境下安全执行,因为有跨线程调用同一个组件方法的状况存在。若该组件接口是线程安全的,则无须套间,否则需要套间的协助,就如窗口过程函数一样,
窗口过程本身并不是线程安全的,但是消息队列的机制,保证了窗口过程总是在一个线程中执行,串行地处理消息。
Windows的消息机制是通过窗口来实现的,那么一个线程要接收消息,也应该有一个窗口。
COM API的设计者在它们的API函数中实现了一个隐藏的窗口。在我们调用CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)的时候,会生成这个窗口。该窗口是隐藏的,有了这个窗口,就可以支持消息机制,就有办法来实现对象中函数的逐一执行。这样当对象指针被传到其它线程的时候,从外部调用该对象的方法的时候,就会先发一个消息到原线程,
而不再直接访问对象。
这样设计的机制保证COM组件对象中的方法总是在一个线程中被调用(串行的),是线程安全的。
1.套间与线程
   CoInitializeEx是一个创建套间的过程,我们使用CoInitializeEx(NULL, COINIT_MULTITHREADED)后,会创建一个MTA套间。CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)创建个STA套间。一个进程可以包含多个STA,但只能有一个MTA。 一个STA只能包含一个线程,一个MTA可以包含多个线程。
2.COM组件的线程模型(Threading Model)共有四种:Single Apartment Both Free(可以通过修改注册表直接改变这个组件的Treading Model) 
   2.1 Single:
       当一个组件的线程模型被标识为Single,说明进程中这个组件的实例 都必须在同一个套间线程中。(该套间线程就是进程创建的第一个STA套间线程)。无论在进程中创建多少个组件实例,接口调用都是在同一个线程中完成的。这 其中当然涉及到给隐藏窗口发送消息和等待,需要注意的是避免线程间的死锁问题。
       2.2 Apartment:
       当一个组件的线程模型被标识为Apartment,当创建了一个组件,并把组件的某个接口传递给另一个线程时,在这个两个线程中调用这个接口提供的方法,最终都是在同一个线程中完成的。说明该组件不是线程安全的,需要套间的协助。所处的套间线程必须是COINIT_APARTMENTTHREADED。
       2.3 Free
       说明组件是线程安全的,当发生2.2的状况时,调用是在不同线程中完成的。但是所处套间必须是MTA(COINIT_MULTITHREADED)
       2.4 Both
       说明组件是线程安全的,所处的套间MTA(COINIT_MULTITHREADED)和STA(COINIT_APARTMENTTHREADED)都可以。
3.缺省套间线程
       当在创建组件时所处的线程(所创建的套间)与该组件的线程模型不匹配。这种状况下系统就会把组件对象放入缺省的套间中(当然是运行在一个缺省的线程中).
4.套间的本质
    An apartment is neither a process nor a thread; however, apartments share some of the properties of both。套间是保存在线程的TLS中的一个数据结构,借用该结构使套间和线程之间建立起某种关系,通过该结构可以帮助不同的套间之间通过消息机制来实现函数的调用,以保证多线程环境下,数据的同步。
一、线程、Apartment和进程  
说道COM的线程模型,大家就会想到各种Apartment模型。但Apartment究竟是什么?如何建立一个Apartment呢?  
Apartment就是线程的容器,线程中有关COM的操作必须在Apartment中进行。Apartment分为STA和MTA两种,STA是只能容纳一个线程的容器,MTA是能容纳多个线程的容器。COM规定,一个进程中可以有多个STA,但最多只能有一个MTA。线程调用CoInitializeEx(NULL,COINIT_APARTMENTTHREADED)后,这个线程就建立并且进入了一个STA,线程调用CoInitializeEx(NULL,COINIT_MULTITHREADED)后,这个线程就进入了进程公用MTA。一个线程不能同时进入两个Apartment。线程调用CoUninitialize()后,这个线程就退出了它所在的Apartment。设计COM对象时设定的“Apartment模型”就是指这个COM对象可以呆在那种Apartment中。一个线程建立的COM对象自动地呆在这个线程所在的Apartment中。要是这个线程建立了很多个COM对象,那这些对象都呆在这个线程所在的Apartment中。  
一个线程可以直接访问它所在的Apartment中的COM对象,但要访问另一个Apartment中的COM对象就必须经过调度。因为STA中只有一个线程,别的线程要访问这个线程建立的COM对象就必须让这个线程代劳了,如此一来,对这个Apartment中所有的COM对象的访问都是序列化的,这些COM对象就不用担心有好几个线程同时访问它的麻烦事。MTA中的COM对象就没这么舒服了,它们必须考虑到可能会有好几个线程同时访问它们。MTA之外的一个线程访问MTA中的一个COM对象时,系统会从COM系统线程池中取出一个线程进入MTA,由它来代表客户线程访问这个COM对象。(COM系统线程池的机理是怎么样的?池中有几个线程?)  
二、客户与服务器  
COM对象位于服务器中,服务器分为进程内服务器、进程外服务器、远程服务器三种。进程内服务器是一个DLL文件,进程外服务器是一个EXE文件,远程服务器是另一台计算机上的一个DLL文件或EXE文件。远程服务器如果是一个DLL文件的话,由一个被称为“Surrogate”的代理程序调用它。  
进程内服务器中的COM对象的Apartment模型如果与客户线程所在的Apartment相配合的话,客户线程建立COM对象时会直接建立在客户线程所在的Apartment中。比如Apartment模型与STA、Free模型与MTA,Both模型与STA或MTA。这样客户线程就可以直接调用COM对象而不用调度。否则就会专门建立一个线程,然后由这个线程建立COM对象,COM对象和客户线程就分处在两个Apartment中。进程外服务器和远程服务器中的COM对象一定不会建立在客户线程所在的Apartment中。对它们的调用一定要经过调度的。  
三、在C++Builder下建立一个多Apartment的进程外服务器  
由于不必考虑并行的问题,COM对象一般设成使用Apartment线程模型。进程内服务器还没什么问题,如果你试着建了一个进程外服务器,并且让几个客户同时访问服务器中的对象的话,就会发现这些访问不是同时进行的。如果有一个访问特别费时间,它后面的访问就要等很久才能进行。这是因为服务器中只有一个STA,虽然每个线程都建立了自己的COM对象,但这些对象都在这个STA中,当然无法并行执行。  
克服这个问题的办法很简单,打开Borland\CBuilder5\Include\Atl\Atlmod.h文件,把第266行的:  
typedef TATLModule TComModule;  
改成:  
#ifdef __DLL__  
typedef TATLModule TComModule;  
#else  
typedef TATLModule > TComModule;  
#endif  
再打开Borland\CBuilder5\Include\Atl\Atlcom.h文件,把第3214行的:  
DECLARE_CLASSFACTORY()  
改成:  
#ifdef __DLL__  
DECLARE_CLASSFACTORY()  
#else  
DECLARE_CLASSFACTORY_AUTO_THREAD()  
#endif  
就可以了。重新编译你的程序,同时开两个客户试一试,是不是并发执行了?  
先别高兴得太早,如果你同时开了五个客户,并且其中四个在执行费时的访问,你就会发现第五个客户的访问要等待一段时间。这种现象与C++Builder的实现代码有关。  
作了前面的修改后,服务器启动后会预先生成几个线程,这些线程各自进入一个STA中。当服务器接到客户的访问要求后,会循环指定一个线程负责这个客户的建立COM对象、访问COM对象的事务。  
比如第一个客户要求建立一个COM对象,服务器就给一号线程发消息,让这个线程建立一个COM对象并把这个COM对象的接口传给客户,以后第一个客户对这个COM对象的访问就全由一号线程代理。而第二个客户的建立COM对象、访问COM对象的事务就由服务器指定二号线程来办,如果客户太多,线程用完了,服务器又会让一号线程负责客户的要求,依次循环。如果客户很多,线程可能会负责几个客户的访问要求,而由同一个线程服务的客户的访问就会顺序执行。预先生成的线程数缺省为系统的CPU个数乘以四,也就是四个(除非你的机器有好几个CPU)。  
只能同时服务四个客户当然是不行的,让我们继续修改。打开主CPP文件,可以看到下面两行代码:  
TComModule ProjectModule(0);  
TComModule &_Module = ProjectModule;  
改为:  
TComModule ProjectModule(MyInitATLServer);  
TComModule &_Module = ProjectModule;  
其中“MyInitATLServer”是一个新加的函数,定义如下:  
void __fastcall MyInitATLServer()  
 
if (_Module.SaveInitProc)  
_Module.SaveInitProc();  
_Module.Init(ObjectMap, Sysinit::HInstance, NULL, 6);//注意这个6  
_Module.m_ThreadID = ::GetCurrentThreadId();  
_Module.m_bAutomationServer = true;  
_Module.DoFileAndObjectRegistration();  
AddTerminateProc(_Module.AutomationTerminateProc);  
 
看到那个6没有,这代表服务器启动后会预先生成6个线程,也就能同时服务6个客户。这个6可以改成别的数,当然不要太大了,不然机器垮了可别怪我。  
改到现在你可能比较满意了,但其实这个服务器还是有缺陷:一开始就生成所有线程是不是太浪费了?循环分配线程好象也不太合理,更重要的是,如果客户程序中途垮了,没有Release它建立的COM对象,那这个COM对象将一直存在下去,占用的资源无法收回。  
要解决这些问题就比较麻烦了,建议大家看一看ATL源代码,编写自己的TComModule类和CComThreadAllocator类。  
四、编写多线程客户程序时要注意的问题  
建立客户程序时必须包含的*_ATL.h文件中有一个很好的COM对象包装类。比如我建立了一个ComLib服务器,里面有一个MyComObj对象,那么在ComLib_ATL.h文件中有一个TCOMIMyComObj类,它很好的封装了MyComObj对象。写单线程程序时可以这样建立它:  
TCOMIMyComObj aComObj = CoMyComObj::CreateInstance();  
(CoMyComObj是定义在在ComLib_ATL.h文件中的一个辅助类)然后就可以使用aComObj了,不必调用CoInitializeEx()和CoUninitialize(),也不必释放aComObj。假设MyComObj对象中定义了一个方法fun(),一个属性num,可以这样使用:  
aComObj.fun();  
aComObj.num = 14;  
int val = aComObj.num;  
注意到num的访问方法了吗?C++Builder灵活运用了特有的__property关键字,不必调用get_num()和set_num()了。  
如果在写多线程客户程序时也这样就会出问题:除了第一个线程正常外,后面的的线程无法建立COM对象了。  
问题出在CoMyComObj里面,它保证了会调用CoInitializeEx()和CoUninitialize()并且在整个进程中只会调用一次。而在多线程客户程序中,每个线程都必须调用CoInitializeEx()和CoUninitialize()一次。因此,除了第一个线程成功进入了Apartment,别的线程都失败了。  
可以这样建立TCOMIMyComObj对象:  
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);  
IMyComObj *pComObj;  
OleCheck(CoCreateInstance(CLSID_MyComObj, NULL, CLSCTX_LOCAL_SERVER  
, IID_IMyComObj, (void **)(&pComObj)));  
TCOMIComObjInExe aComObj(pComObj);  
……使用aComObj……  
CoUninitialize();  
注意,这段代码必须写在TThread::Execute()中,因为只有TThread::Execute()里的代码才是真正运行在新线程中的。另外决不能调用pComObj->Release()。  
后记  
学COM的念头起于看李维写的那三本书中的第一本的时候,李维描述了建立多线程服务器的重要性,但具体方法只是一笔带过。后来我看了Delphi带的例子,想用在C++Builder中,却无从下手。在关于COM的部分,Delphi和C++Builder相差太大了,而又没有这方面的C++Builder的书,网上的资料也很少,只好自己摸索。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值