Sunspot Lee
一、线程、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<CComModule> TComModule;
改成:
#ifdef __DLL__
typedef TATLModule<CComModule> TComModule;
#else
typedef TATLModule<CComAutoThreadModule<CComSimpleThreadAllocator> > 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的书,网上的资料也很少,只好自己摸索。期间曾在网上发贴提问,总是没人回答,痛苦啊!
……