第二部分,理解com的套間線程
COM套間
為了理解com是如何處理線程的,讀者需要掌握套間的概念!套間在應用中是一個邏輯容器,以使com對象可以分享遵循相同的線程訪問規(guī)則(例在所屬線程中,規(guī)則限定了對象的方法和屬性在有套間和無套間情形中是如何被調(diào)用的)。套間本質(zhì)上只是一個概念,不像對象有自己的屬性和方法。沒有句柄類型可以引用它,更沒有可調(diào)用的API操縱它。對于新手來說,這或許是套間難理解的一個重要原因,它是如此的抽象。
如果存在CoCreateApartment()和一些諸如CoEnterApartment()的API函數(shù),如果微軟提供帶有IApartment接口和操縱線程、對象方法的COM類,套間也許會很容易理解。從編程來說,似乎沒有切實的方式去洞察套間。為了幫助新手克服剛開始學習套間遇到的困難,提供以下幾點參考:
1、套間由應用創(chuàng)建,沒有直接創(chuàng)建或者檢查它們存在的函數(shù)。
2、線程和對象也通過應用進入套間并且參與套間相關的活動,沒有現(xiàn)成的函數(shù)完成這一過程。
3、套間模型十分像協(xié)議,或者需遵循的規(guī)則集合。
在多線程訪問眾多com對象的操作系統(tǒng)中,我們?nèi)绾文鼙WC一個線程對com對象屬性、方法的調(diào)用結(jié)果不受另一線程對該對象訪問的影響?為了解決上述問題,com套間應運而生,它的出現(xiàn)就是為了保證所謂的現(xiàn)線程安全。通過套間,我們就可以保證本線程訪問對象的內(nèi)部狀態(tài)免受其它線程訪問的影響。
com中包含三種套間模型:單線程套間、多線程套間和中性套間(Neutral Apartment),每一類型套間代表了一種跨線程同步對象內(nèi)部狀態(tài)的機制。對于線程和對象,套間遵循以下的基本原則:
1、一個com對象僅且只能存在于一個套間。運行時對象一經(jīng)創(chuàng)建就確定所屬套間,并且直到銷毀它一直存在于這個套間。
2、一個com線程(內(nèi)部創(chuàng)建了 com對象或者調(diào)用了com方法的線程)也屬于一個套間。與com對象一樣,線程從創(chuàng)建到結(jié)束都存在于同一套間。
3、屬于相同套間的線程和對象遵循相同的線程訪問規(guī)則。套間內(nèi)部方法的調(diào)用是直接完成的,不需要com額外的輔助。
4、不同套間的線程和對象遵循了不同的線程訪問規(guī)則。跨套間方法調(diào)用通過列集實現(xiàn),這就需要采用proxies和stubs。
除了保證線程安全,套間的另一好處是透明性,對象和客戶端都不需要關心他們采用的套間模型。底層的套間實現(xiàn)細節(jié)(特別是列集機制)由com子系統(tǒng)管理,開發(fā)者無需關心。
(二)COM對象套間模型設定
從本段開始一直到“EXE COM服務器和套間”,期間涉及的com對象都是在DLL服務器中實現(xiàn)。正如上面提到的,com對象僅屬于一個運行的套間,在對象創(chuàng)建時就已經(jīng)確定。然而首先需要明白的是一個com對象是如何關聯(lián)它的套間模型的?對于DLL服務器中的com類,當com線程實例化它時,該類會參考注冊表“InProcServer32”字段項“ThreadingModel”的字符串值。這個設置有開發(fā)者控制。比如,當你使用ATL開發(fā)com對象時,你可以指定對象在運行時采用的線程模型。下面列舉了線程模型字符串值和代表的套間模型:
序號 注冊表值 套間模型
1 “Apartment” 單線程套間
2 “Single”或者空 Legacy單線程套間
3 “Free” 多線程套間
4 “Neutral” Neutral套間
5 “Both” 創(chuàng)建線程的套間模型
“Both”字符串值說明com對象適用于單線程套間和多線程套間。
(三)COM線程套間模型設定
每一個com線程必須通過CoInitializeEx()函數(shù)并且設定參數(shù)為COINIT_APARTMENTTHREADED或者COINIT_MULTITHREADED進行自身初始化。訪問了CoInitializeEx()函數(shù)的線程即為com線程,也即線程已經(jīng)進入套間。直到線程調(diào)用CoUninitialize()函數(shù)或者自身終止,才會離開套間。
中性線程套間(NTA)
Windows 2000引入了NTA用于性能優(yōu)化。進行跨套間方法調(diào)用時,進入STA和MTA的方法調(diào)用引起的線程切換會占用大量開銷。而進入NTA的調(diào)用不會引起線程切換。如果STA或者MTA線程調(diào)用同一個進程中基于NTA的對象,線程會暫時離開其套間,直接執(zhí)行NTA中的代碼。
注:因為在某些場合下,我們很有可能會遭遇到NTA的com組件,所以適當留意一下。
單線程套間(STA)
注:因為VBA只能創(chuàng)建和使用單線程套間,所以理解單線程套間是重點。
單線程套間只能包含一個線程(因此才稱為單線程套間),但是可以包含多個對象。包含在單線程套間中的線程有一特別之處——如果線程中的對象需要導出給其它線程,那么它必須有自己的消息循環(huán)。線程通過調(diào)用CoInitializeEx()并指定函數(shù)參數(shù)為COINIT_APARTMENTTHREADED或者簡介的調(diào)用CoInitizlize()函數(shù)(CoInitialize()函數(shù)實際上會調(diào)用參數(shù)設定為COINIT_APARTMENTTHREADED的CoInitializeEx())進入單線程套間。進入單線程套間的線程也可認為已經(jīng)創(chuàng)建了那個套間(畢竟,在套間內(nèi)沒有其它線程第一次創(chuàng)建它)。通過指定“Apartment”到注冊表合適位置和在單線程套間內(nèi)實例化對象,我們可以說com對象進入單線程套間。
(一)STA線程訪問規(guī)則
STA的線程訪問規(guī)則如下:
1、所有的STA對象如STA線程一樣屬于相同的單線程套間。
2、STA內(nèi)的所有對象只接受該STA線程的方法調(diào)用。
第一點理解起來十分自然和簡單。然而,請注意在相同DLL服務器、不同STA線程中創(chuàng)建的同一com類的兩個對象屬于不同的套間。訪問這兩個對象輸入跨套間,必須通過com的列集完成。至于第二點,有兩種方式可以訪問STA對象:1、STA線程內(nèi)部訪問。這種情形方法的調(diào)用被序列化。2、跨線程訪問(也即跨套間)。這種情形下STA線程必須包含消息循環(huán),COM才能保證對象僅接受自身STA線程的方法訪問。
(二)STA中的消息循環(huán)
重要規(guī)則:STA線程需要消息循環(huán)
如果不理解單線程套間機制,這條規(guī)則看起來不那么明顯?蛻粽{(diào)用基于STA的對象時,調(diào)用將被傳遞到STA中運行的線程。COM通過向STA的隱藏窗口投遞消息來完成這種傳遞。那么,如果STA中的線程不接收和分發(fā)消息將發(fā)生什么?調(diào)用將在RPC通道中消失,永遠也不返回。它將永遠凋謝在STA的消息隊列中。
擁有消息循環(huán)的線程是UI線程,它關聯(lián)著一個或多個窗口,即線程擁有這些窗口。窗口對應的窗口過程由所屬線程創(chuàng)建。任何線程發(fā)送或者發(fā)布消息給所有的窗口,但是只有目標窗口過程才會響應這些消息。發(fā)往目標窗口的消息被同步,即窗口會保證按照消息發(fā)送或發(fā)布的順序接受處理消息。對于Windows程序開發(fā)者來說,窗口處理過程不必是線程安全的。每一個窗口消息會轉(zhuǎn)化成原子操作,只有當前消息處理完才能處理下一消息。在Windows系統(tǒng)中,這給com帶來與生俱來的優(yōu)點:使得com對象輕松的實現(xiàn)線程安全。COM通過發(fā)布私有消息給對象關聯(lián)的隱藏窗口,實現(xiàn)外部套間對STA對象方法的調(diào)用。隱藏窗口的窗口過程安排處理對象的訪問并且把結(jié)果返回調(diào)用者。
需要注意的是當涉及到外部套間時,COM總是引入porxies和stubs,如消息循環(huán)一樣兩者形成了單線程套間協(xié)議的一部分。有兩個重要的知識點需要關注:
1、系統(tǒng)只有當訪問來自外部套間時,采用消息循環(huán)激發(fā)STA對象的方法才是可使用的。如果是來自套間內(nèi)的訪問,完全不需要com的參與,STA線程自身會保證調(diào)用以串行方式執(zhí)行。
2、如果STA線程從消息隊列中獲取或分發(fā)消息失敗,線程套間內(nèi)的com對象將無法接受套間之間的訪問。
考慮到上述第2點,諸如Sleep()、WaitForSingleObject()、WaitForMultipleObjects()等影響線程消息處理順序的函數(shù)是非常重要的。如果STA線程需要等待同步對象,為了保證消息循環(huán)不被打亂,必須采取特殊處理。
實際應用中某些情形STA線程不需要包含消息循環(huán)。
(三)STA優(yōu)缺點
使用STA最大優(yōu)點就是簡潔。對于com對象服務器來說,除了一些基本的代碼,參與的com對象和線程幾乎不需要同步代碼。com中所有方法的調(diào)用會自動串行。因STA對象總是由相同線程訪問,即具有線程相似性。正是線程相似性,使得STA對象開發(fā)者能夠采用線程局部存儲保存對象內(nèi)部數(shù)據(jù)的狀態(tài)。VB和MFC采用這種開發(fā)技術,所以它們開發(fā)的對象是單線程套間對象。在需要支持legacy com組件的情形中采用STA是不可避免的。
任何事物都有兩面性,當然使用STA也不例外點。多個線程訪問同一com對象時STA架構(gòu)嚴重降低了性能。由于每一線程訪問對象都會被串行化,所以線程必須等待其他占用線程的返回。等待時間降低了應用響應或者性能。如果線程包含過多的對象時,STA架構(gòu)也會降低性能。切記單線程套間包含一個線程和一個消息隊列,因而STA內(nèi)各個對象的訪問由消息隊列串行化。
鑒于STA的優(yōu)缺點,使用時必須根據(jù)實際應用場景來選擇。
(四)STA COM對象及其服務器實現(xiàn)
對于開發(fā)者,STA com對象的實現(xiàn)一般不必關心內(nèi)部成員數(shù)據(jù)的串行化訪問。然而,單線程套間本身無法確保com DLL服務器的全局數(shù)據(jù)和導出的全局函數(shù)(如DllGetClassObject()和DllCanUnloadNow())線程安全。切記com服務器對象可以在任何線程中創(chuàng)建,相同Dll服務器中的兩個對象可以在各自獨立的STA線程中創(chuàng)建,這保證了無需com的串行化機制服務器的全局數(shù)據(jù)和函數(shù)可以由兩個不同線程正確訪問。
另外,這種情形下線程的消息循環(huán)也無法提供任何幫助,畢竟這不是對象的內(nèi)部狀態(tài)(如果是程序就要付出代價啦),而是服務器內(nèi)部狀態(tài)。由于不同線程的對象可能訪問服務器的全局變量和全局函數(shù),因而它們需要適當串行化。這一規(guī)則也適用于類的靜態(tài)成員函數(shù)和靜態(tài)成員變量。
一個眾所周知的com服務器變量是全局對象計數(shù),其由常用的全局函數(shù)DllGetClassObject()和DllCanUnloadNow()調(diào)用。API函數(shù)InterlockedIncrement()和InterlockedDecrement()可以同步不同線程對全局對象計數(shù)的訪問。
總結(jié)STA服務器DLL實現(xiàn)的一般原則:
1、服務器dll必須有線程安全的標準入口函數(shù),比如函數(shù)DllGetClassObject()和DllCanUnloadNow()。
2、服務器dll的私有全局函數(shù)必須是線程安全的。
3、私有全局變量(特別是全局對象計數(shù))必須是線程安全的。
DllGetClassObject()函數(shù)功能是提供類對象的訪問。類對象根據(jù)CLSID返回,并且通過它的接口(通常是IClassFactory)指針引用自身。DllGetClassObject()函數(shù)在API函數(shù)CoGetClassObject()內(nèi)部調(diào)用,com開發(fā)者無需直接調(diào)用。通過類對象的CLSID創(chuàng)建對象實例(由IClassFactory::CreateInstance()),我們可以把DllGetClassObject()函數(shù)視為com對象創(chuàng)建的開關,請記住該函數(shù)最重要的一點:影響全局對象計數(shù)。我們調(diào)用函數(shù)DllGetClassObject(),可以獲得標志com服務DLL包含對象是否仍舊存在和發(fā)揮功效。DllGetClassObject()函數(shù)根據(jù)全局對象計數(shù)決定返回值,如果com服務器dll中沒有對象存在,調(diào)用該函數(shù)可以從內(nèi)存中卸載com服務器DLL。
函數(shù)DLLGetClassObject()和DllCanUnLoadNow()必須是線程安全的以同步全局對象計數(shù)。一般來說,當一個對象創(chuàng)建或者銷毀的時候,全局對象計數(shù)相應增加或者減少。本文沒有對私有全局函數(shù)和全局變量的線程安全進行詳細介紹,只能留給專家和經(jīng)驗豐富者去探討。
對于com服務器來說,保證線程安全不是復雜的過程,許多情形下只需要簡單的思考。上面的這些講解對于ATL COM服務器開發(fā)者已經(jīng)足夠(除了私有全局數(shù)據(jù)和全局函數(shù)的線程安全),所以他們只需要關注com對象的業(yè)務邏輯開發(fā)。
(五)STA線程實現(xiàn)
STA線程通過調(diào)用CoInitialize()或者CoInitializeEx(COINIT_APARTMENTTHREADED)進行初始化自身。其次,如果線程創(chuàng)建的對象需要導出到其它線程(亦即其它套間),該線程需要提供消息循環(huán)處理com對象隱藏窗口的消息。切記com中隱藏窗口的窗口過程接受和處理這些私有消息,STA線程自身不會處理。如下的代碼示例了STA線程的框架:
DWORD WINAPI ThreadProc(LPVOID lpvParamater) { /* Initialize COM and declare this thread to be an STA thread. */ ::CoInitialize(NULL); ... ... ... /* The message loop of the thread. */ MSG msg; while (GetMessage(&msg, NULL, NULL, NULL)) { TranslateMessage(&msg);
DispatchMessage(&msg); } ::CoUninitialize(); return 0; }
STA線程十分像WinMain()函數(shù),實際上Windows應用的WinMain()函數(shù)也運行在線程中。你可以實現(xiàn)像WinMain()函數(shù)一樣的STA線程,即在消息循環(huán)之前創(chuàng)建窗口并保證窗口有合適的窗口過程。當然,在窗口過程中你可以創(chuàng)建和管理com對象,也可以跨套間訪問外部STA對象。如果你不想在線程中創(chuàng)建窗口,你仍然可以創(chuàng)建操縱對象并且跨套間訪問外部線程的方法。
(六)不需要消息循環(huán)的STA線程
STA線程有時是不需要消息循環(huán)的,比如線程僅僅創(chuàng)建和使用對象而不供其它套間訪問的情況。示例如下:
int main() { ::CoInitialize(NULL); if (1) { ISimpleCOMObject1Ptr spISimpleCOMObject1; spISimpleCOMObject1.CreateInstance(__uuidof(SimpleCOMObject1)); spISimpleCOMObject1 -> Initialize(); spISimpleCOMObject1 -> Uninitialize(); } ::CoUninitialize(); return 0;
}
上面的控制臺程序示例了主線程中創(chuàng)建STA而不需要消息循環(huán)。注意我們可以成功的調(diào)用Initialize()函數(shù)和Uninitialize()函數(shù),因它們在STA內(nèi)部調(diào)用不要列集和消息循環(huán)的參與。但是,如果我們調(diào)用::CoInitializeEx(NULL, COINIT_MULTITHREADED)函數(shù),main()函數(shù)變成了多線程套間,同時程序發(fā)生了以下變化:
1、Initialize()函數(shù)和Uninitialize()函數(shù)的調(diào)用需要com列集的協(xié)助。
2、com對象spISimpleCOMObject1屬于com子系統(tǒng)創(chuàng)建的默認STA套間而不是MTA套間。
3、main()線程本身仍然不要消息循環(huán),但是默認STA需要。
4、調(diào)用Initialize()函數(shù)和Uninitialize()函數(shù)需要消息循環(huán)參與。
切記如果STA線程需要消息循環(huán),一定要保證該消息循環(huán)穩(wěn)定而不受中斷的執(zhí)行。
(七)示例聚焦STA
下面我們重點講解單線程套間。采用的方法是觀察com對象方法被調(diào)用時執(zhí)行線程的ID,對于標準STA對象,此ID就是STA對象的ID。如果一個STA對象不屬于創(chuàng)建它的線程(即這個線程不是STA線程),那么該線程ID與執(zhí)行對象方法的線程ID不匹配。
標準STA
以一個簡單的例子講解標準STA。如下所示,例子包括一個簡單的STA com對象,對應的com類CSimpleCOMObject2實現(xiàn)了TestMethod1()方法。TestMethod1()顯示正在執(zhí)行線程的ID消息框,代碼如下:
STDMETHODIMP CSimpleCOMObject2::TestMethod1() { TCHAR szMessage[256]; sprintf (szMessage, "Thread ID : 0x%X", GetCurrentThreadId()); ::MessageBox(NULL, szMessage, "TestMethod1()", MB_OK); return S_OK; }
我們通過一個簡單的測試程序?qū)嵗疌SimpleCOMObject2并調(diào)用方法TestMethod1,代碼如下:
int main() { HANDLE hThread = NULL; DWORD dwThreadId = 0; ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); DisplayCurrentThreadId(); if (1) { ISimpleCOMObject2Ptr spISimpleCOMObject2; spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2)); spISimpleCOMObject2
-> TestMethod1(); hThread = CreateThread ( (LPSECURITY_ATTRIBUTES)NULL, // SD (SIZE_T)0, // initial stack size (LPTHREAD_START_ROUTINE)ThreadFunc, // thread function (LPVOID)NULL, // thread argument (DWORD)0, // creation
option (LPDWORD)&dwThreadId // thread identifier ); WaitForSingleObject(hThread, INFINITE); spISimpleCOMObject2 -> TestMethod1(); } ::CoUninitialize(); return 0; }
線程函數(shù)ThreadFunc()函數(shù)如下:
DWORD WINAPI ThreadFunc(LPVOID lpvParameter) { ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); DisplayCurrentThreadId(); if (1) { ISimpleCOMObject2Ptr spISimpleCOMObject2A; ISimpleCOMObject2Ptr spISimpleCOMObject2B; spISimpleCOMObject2A.CreateInstance(__uuidof(SimpleCOMObject2));
spISimpleCOMObject2B.CreateInstance(__uuidof(SimpleCOMObject2)); spISimpleCOMObject2A -> TestMethod1(); spISimpleCOMObject2B -> TestMethod1(); } ::CoUninitialize(); return 0; }
線程函數(shù)中包含了展示當前線程ID消息框的函數(shù)DisplayCurrentThreadId(),其代碼如下:
/* Simple function that displays the current thread ID. */ void DisplayCurrentThreadId() { TCHAR szMessage[256]; sprintf (szMessage, "Thread ID : 0x%X", GetCurrentThreadId()); ::MessageBox(NULL, szMessage, "TestMethod1()", MB_OK); }
上面的例子創(chuàng)建了單線程套間,通過線程ID來解釋這一過程。從main()函數(shù)開始,詳細分析如下:
1、main()函數(shù)通過調(diào)用CoInitializeEx和參數(shù)COINIT_APARTMENTTHREADED進入單線程套間。由此在main()中創(chuàng)建的STA對象屬于main()線程的一部分。
2、main()函數(shù)調(diào)用DisplayCurrentThreadId()函數(shù),此時會顯示main()線程的ID,設為thread_id_1。
3、隨后程序?qū)嵗薱om類SimpleCOMObject2,該對象和main()線程屬于相同的STA。
4、spISimpleCOMObject2的方法TestMethod1()顯示的線程ID肯定也是thread_id_1。隨后啟動了線程和線程函數(shù)ThreadFunc(),主程序調(diào)用WaitForSingleObject()等待線程函數(shù)ThreadFunc()的結(jié)束。
5、線程函數(shù)ThreadFunc()再次調(diào)用CoInitializeEx和參數(shù)COINIT_APARTMENTTHREADED,同時進入單線程套間。注意這里的STA套間是新創(chuàng)建的,不同于main()函數(shù)的STA。
6、線程函數(shù)中調(diào)用了DisplayCurrentThreadId()函數(shù),此時顯示的是ThreadFunc()線程的ID,設為thread_id_2。
7、隨后我們又創(chuàng)建了兩個com對象(spISimpleCOMObject2A和spISimpleCOMObject2B)。
8、線程函數(shù)中調(diào)用了兩個對象的方法TestMethod1()。
9、當對象spISimpleCOMObject2A和spISimpleCOMObject2B調(diào)用各自的方法時運行線程ID分別顯示。
10、它們顯示和函數(shù)ThreadFunc()相同的線程ID,即thread_id_2。
11、線程結(jié)束后放回主程序。
12、主程序中又一次調(diào)用方法TestMethod1(),當然顯示的線程ID依然是thread_id_1。
通過示例需要記住的是:同一com類的不同實例可以屬于不同獨立的STA。對于一個標準的STA模型,重要的是哪個套間實例化了該STA對象。這個例子不需要提供任何的消息徐循環(huán),因為各對象都在他們自己的套間內(nèi),并沒有跨線程調(diào)用。即使我們在main()中調(diào)用了WaitForSingleObject()也不會遇到任何麻煩。
默認STA
假如STA對象在一個非STA線程中創(chuàng)建情形會如何?下面的代碼示例了此種情況,其中com類 SimpleCOMObject2和函數(shù)DisplayCurrentThreadId()同第一個例子。
int main() { ::CoInitializeEx(NULL, COINIT_MULTITHREADED); DisplayCurrentThreadId(); if (1) { ISimpleCOMObject2Ptr spISimpleCOMObject2; /* If a default STA is to be created and used, it will be created */ /* right after spISimpleCOMObject2 (an STA object) is
created. */ spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2)); spISimpleCOMObject2 -> TestMethod1(); } ::CoUninitialize(); return 0; }
程序的詳細執(zhí)行過程如下:
1、main()函數(shù)調(diào)用CoInitializeEx(NULL, COINIT_MULTITHREADED),主線程初始化自身為多線程套間。
2、隨即程序調(diào)用DisplayCurrentThreadId()函數(shù),顯示了main()線程的ID。
3、接著STA對象spISimpleCOMObject2在線程中實例化。
4、注意對象spISimpleCOMObject2在非STA線程中初始化,它不屬于main()線程的MTA,屬于默認的STA。
5、調(diào)用spISimpleCOMObject2對象的方法TestMethod1(),此時顯示的線程ID不是main()線程ID。
當所有的STA對象在一個非STA線程中創(chuàng)建時,它們自動歸屬于一個默認的STA,該STA是在創(chuàng)建對象時創(chuàng)建的。這時在創(chuàng)建對象的線程中獲得是對象的代理而非指針。注意,默認的STA必須包含消息循環(huán),由com提供。
做為com套間世界的新人,必須注意:即使訪問createInstance或者CoCreateInstance函數(shù),生成的對象有可能是在另一線程實例化的。com的這種行為對于用戶是完全透明的,請注意這些小的細節(jié),特別是在調(diào)試時。
Legacy STA
Legacy STA屬于默認的單線程套間,其對象屬于legacy com對象。Legacy意味著那些組件沒有任何線程的知識,這些組件必須有設置成“Single”的ThreadingModel注冊表或者其它注冊表。Legacy STA對象比較重要的一點是這些對象的實例必須在相同的STA中創(chuàng)建,它們一直存在和運行在這個legacy STA中,即使它們創(chuàng)建在以::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)初始化的線程中。
Legacy STA通常是進程中的第一個單線程套間,如果一個legacy STA對象創(chuàng)建在任何STA模型之前,一個Legacy STA由com子系統(tǒng)自動創(chuàng)建。開發(fā)legacy STA對象好處在于這些對象實例的訪問將被串行化,任何兩個legacy STA對象間的調(diào)用不要列集的參與。然而,非legacy STA的對象必須通過inter-apartment列集訪問legacy STA對象,反之也是如此。我認為這不是一個有吸引力的優(yōu)點。
另外,所有的Legacy STA對象必須僅能在相同的STA線程創(chuàng)建。
EXE COM服務器和套間
討論完Dll com服務器,為了文章的完整性接著介紹EXE COM服務器。首先介紹DLL服務器和EXE服務器的兩個重要差別。
差別1:對象創(chuàng)建的方式
當COM創(chuàng)建定義在DLL中的com對象時,必須加裝該dll,調(diào)用導出函數(shù)DllGetClassObject()獲得com對象工廠類的IClassFactory接口指針。EXE服務器實際上也遵循這個過程:獲得com對象工廠類的IClassFactory接口指針并通過它創(chuàng)建對象。那么DLL服務器和EXE服務器差別在那里?
DLL服務器需要導出函數(shù)DllGetClassObject()以便com能夠提取類工廠,而EXE服務器無需導出任何函數(shù),但要在啟動時向com子系統(tǒng)注冊類工廠,退出時銷毀。這一注冊過程通過調(diào)用API函數(shù)CoRegisterClassObject()實現(xiàn)。
差別2:對象套間模型聲明的方式
本文前面提到DLL服務器中的對象通過設置“InProcServer2”注冊表中的“ThreadingModel”字符串項聲明自己的套間。EXE服務器中的對象不用設置注冊表,注冊對象類工廠的線程的套間模型決定了對象套間模型。
除了上述兩點差別,讀者還應該注意的是DLL服務器中的STA對象有可能只服務于線程內(nèi)的調(diào)用,而來客戶端對EXE服務器對象的所有方法調(diào)用都以跨線程的方式實現(xiàn),這一過程需要使用列集和stubs,以及對象所屬套間線程的消息循環(huán)。