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的書,網上的資料也很少,只好自己摸索。期間曾在網上發貼提問,總是沒人回答,痛苦啊!
|