天天看點

COM的八個經驗和教訓COM的八個經驗和教訓

COM的八個經驗和教訓

總是調用CoInitialize(Ex)

硬體公司編寫了一個非常複雜的基于COM的應用程式,其中使用了許多程序内和本地(程序外)的COM元件。在開始時,應用程式建立了COM對象以服務于運作在多線程單元(MTA)中的各種用戶端線程。該對象還可以托管給MTA,這意味着接口指針可以在用戶端線程之間自由交換。測試中發現在應用程式準備關閉之前,一切都進行得不錯。然後,不知是什麼原因,對Release的調用(必須執行此調用,以便正确釋放用戶端占用的接口指針)被鎖定了。問題是:“到底是哪裡出了問題?”

其實答案非常簡單。應用程式的開發人員其他都做得很對,隻有一點例外,而這點又非常重要:他們沒有在所有的用戶端線程中調用CoInitialize或CoInitializeEx。現代COM的基本原則之一,就是每個使用COM的線程都應該先調用CoInitialize或CoInitializeEx來初始化COM。這條原則是無法免除的。除了其他事情以外,CoInitialize(Ex)應将線程放入單元中,并初始化重要的每線程狀态資訊(這對于COM的正确操作是必需的)。調用CoInitialize(Ex)失敗通常會在應用程式生命期早期以失敗的COM API函數的形式表現出來,最常見的是激活請求。但有時問題很隐蔽,直到一切都太晚了(例如對Release的調用一去不複返了)才表現出來。當開發小組将CoInitialize(Ex)調用添加到所有接觸COM的線程之後,他們的問題就迎刃而解了。

具有諷刺意義的是,Microsoft竟是COM程式員有時不調用CoInitialize(Ex)的原因之一。Microsoft知識庫中包含的一些文檔中說,調用CoInitialize(Ex)對基于MTA的線程來說不是必需的(有關示例,請參閱文章Q150777)。是的,在很多情況下,我們可以跳過CoInitialize(Ex)而不會出現問題。但是,這樣是不應該的,除非您知道自己在幹什麼,并且可以絕對肯定自己不會受到負面影響。調用CoInitialize(Ex)是沒有害處的,是以建議COM程式員始終從某個與COM相關的線程中調用它。

不要線上程之間傳遞原始接口指針

首批COM項目之一就涉及到一個包含100,000行代碼的分布式應用程式,該程式是由美國西海岸的一個大型軟體公司編寫的。該應用程式在多個機器上建立了數十個COM對象,并從用戶端程序啟動的背景線程中調用這些對象。開發小組遇到問題了,調用要麼消失得無影無蹤,要麼在沒有明顯原因的情況下失敗。最驚人的症狀是:當一個調用無法傳回時,在同一台機器上啟動其他支援COM的應用程式(包括 Microsoft Paint 等)會頻繁導緻這些應用程式被鎖定。

檢查代碼後發現,他們違反了COM并發的一個基本規則,就是說,如果一個線程要與另一個線程共享一個接口指針,它應首先封送該接口指針。如果有必要,封送接口指針可使COM建立一個新的代理(以及一個新的信道對象,将代理和存根結對),以允許從另一個單元向外調用。不通過封送而将原始接口指針(記憶體中的一個32位位址)傳遞給另一個線程,會繞過COM的并發機制,并且如果發送和接收的線程位于不同的單元中,将出現各種不良行為。(在Windows 2000中,由于兩個對象可以共享一個單元,但又位于不同的上下文中,是以如果線程位于同一個單元中,可能會使您陷入困境。)典型的症狀包括調用失敗和傳回RPC_E_WRONG_THREAD_ERROR。

Windows NT 4.0和更高版本可以使用一對名為CoMarshalInterThreadInterfaceInStream和 CoGetInterfaceAndReleaseStream的API函數,線上程之間輕松地封送接口指針。假定您應用程式中的一個線程(線程A)建立了一個COM對象,繼而接收了一個IFoo接口指針,并且同一程序中的另一個線程(線程B)想調用這個對象。在準備将接口指針傳遞給線程B時,線程A應該封送該接口指針,如下所示:

CoMarshalInterThreadInterfaceInStream (IID_IFoo, pFoo, &pStream);

在 CoMarshalInterThreadInterfaceInStream 傳回後,線程 B 就可以安全地取消封送該接口指針:

IFoo* pFoo;

CoGetInterfaceAndReleaseStream (pStream, IID_IFoo, (void**) &pFoo);

在這些示例中,pFoo是一個IFoo接口指針,pStream是一個IStream接口指針。COM在調用CoMarshalInterThreadInterfaceInStream時初始化IStream接口指針,然後在CoGetInterfaceAndReleaseStream内部使用和釋放該接口指針。實際上,您通常要使用一個事件或其他同步化基元來協調這兩個線程的行為?例如,讓線程 B 知道接口指針已準備好,可以取消封送。

請注意,以這種方式封送接口指針不會出現任何問題,因為COM有足夠的智能,在不需要進行封送時不會去封送(或重新封送)指針。如果線上程之間傳遞接口指針時這樣做,使用COM就輕松多了。

如果調用CoMarshalInterThreadInterfaceInStream和CoGetInterfaceAndReleaseStream看起來太麻煩,您還可以通過将接口指針放在全局接口表(GIT)中,并讓其他線程去那裡檢索它們,進而實作線上程之間傳遞接口指針。從GIT中檢索到的接口指針在被檢索時會自動封送。更多資訊,請參閱IGlobalInterfaceTable中的文檔。請注意,GIT 隻存在于 Windows NT 4.0 Service Pack 4 及更高版本中。

STA線程需要消息循環

上一部分中描述的應用程式還有另一個緻命缺陷。看看您是否能指出來。

這個特殊的應用程式恰好是用MFC編寫的。在一開始,它使用了MFC的AfxBeginThread函數啟動一系列輔助線程。每個輔助線程要麼調用CoInitialize要麼調用AfxOleInit(MFC中類似CoInitialize的函數)來初始化COM。某些輔助線程則調用CoCreateInstance來建立COM對象,并将所傳回的接口指針封送到其他輔助線程。從建立這些對象的線程中調用對象将非常順利,但從其他線程的調用卻從不傳回。您知道這是為什麼嗎?

如果您認為問題與消息循環(或缺少消息循環)相關,那麼答案完全正确。事實确實如此。當一個線程調用CoInitialize或AfxOleInit時,它是放在單線程單元(STA)中。當COM建立一個STA時,它會建立一個随附的隐藏視窗。以STA中的對象為目标的方法調用将轉換為消息,并放入與該STA關聯的視窗的消息隊列中。當運作在該STA中的線程檢索到代表方法調用的消息時,隐藏視窗的視窗過程就會将消息轉換回方法調用。COM使用STA執行調用序列化。STA中的對象一次不可能接收一個以上的調用,因為每個調用都要傳遞給一個而且是惟一一個運作在對象單元中的線程。

如果基于STA的線程無法處理消息會怎麼樣呢?如果它沒有消息循環又會怎麼樣呢?針對該STA中對象的單元間方法調用将不再傳回;它們将在消息隊列中被永遠擱置。MFC輔助線程中沒有消息循環,是以如果寄宿在這些STA中的對象要從其他單元的用戶端接收方法調用,那麼MFC輔助線程和STA是配合不好的。

這個故事的寓意何在呢?STA線程需要消息循環,除非您肯定它們不會包含要從其他線程調用的對象。消息循環可以像這樣簡單:

MSG msg;

while (GetMessage (&msg, 0, 0, 0))

DispatchMessage (&msg);

另一種方案是将COM線程移到MTA中(或者在Windows 2000中,移到中立線程單元,即NTA中),這裡沒有消息隊列依賴項。

單元模型對象必須保護共享資料

另一個困擾COM開發人員的通病是标記為ThreadingModel=Apartment的程序内對象。這項指定告訴COM,對象的執行個體必須隻能在STA中建立。它還可讓COM自由地将這些對象執行個體放在任何主機程序的STA中。

假設用戶端應用程式有五個STA線程,每個線程都使用CoCreateInstance來建立同一個對象的一個執行個體。如果線程是基于STA的,且對象标記為ThreadingModel=Apartment,則這五個對象執行個體将在對象建立者的STA中建立。因為每個對象執行個體都在占用其STA的線程上運作,是以所有五個對象執行個體都可以并行運作。

到目前為止,一切良好。現在考慮一下,如果這些對象執行個體共享資料會發生什麼情況。因為對象都在并發線程上執行,兩個或更多的對象可能會同時嘗試通路同一個資料。除非所有這些通路都是讀取通路,否則就會釀成災難。問題可能不會很快顯現出來;它們會以和時間緊密相關的錯誤形式出現,是以很難診斷和重制。這就解釋了以下事實的原因:ThreadingModel=Apartment對象應該包括可同步對共享資料的通路的代碼,除非您能夠确定對象的用戶端不會對執行通路的方法進行重疊調用。

問題在于,太多的COM開發人員相信ThreadingModel=Apartment能夠使他們免于編寫線程安全的代碼。事實并非如此,至少不完全如此。ThreadingModel=Apartment 并不意味着對象必須是完全線程安全的,它代表的是一個對 COM 的承諾,即通路兩個或更多對象執行個體共享的資料(或此對象和其他對象的執行個體共享的資料)時是以線程安全的方式進行的。而提供該線程安全性的任務應該由您,即對象實作者來負責。共享資料的類型和大小多種多樣,但大多是以全局變量、C++ 類中的靜态成員變量和函數中聲明的靜态變量的形式出現。即使是以下這樣無害的語句也會在 STA 中出問題:

static int nCallCount = 0;

nCallCount++;

因為這個對象的所有執行個體都将共享一個 nCallCount 執行個體,編寫這些語句的正确方式如下:

static int nCallCount = 0;

InterlockIncrement (&nCallCount);

注意:您可以使用臨界區、互鎖函數或您希望的任何方式,但不要忘了通路基于 STA 的對象共享的資料時要進行同步化!

謹慎啟動使用者

這裡還有一個問題讓許多COM 開發人員都吃過苦頭。去年有一家公司使用 COM 建構了一個分布式應用程式,其中用戶端程序運作在與遠端伺服器的 Singleton 對象相連接配接的網絡工作站上。在測試過程中,遇到了一些非常奇怪的行為。在一種測試場景中,用戶端對 CoCreateInstanceEx 的調用可使它們與 Singleton 對象正常連接配接。而在另一個場景中,對 CoCreateInstanceEx 的相同調用産生了多個對象執行個體和多個伺服器程序,使用戶端無法與同一個對象執行個體連接配接,進而實際影響了應用程式。在這兩個場景中,硬體和軟體是完全相同的。

此問題似乎與安全有關。當處理遠端激活請求的 COM 服務控制管理器 (SCM) 在另一台機器上啟動一個程序時,它會為該程序配置設定一個辨別。除非另外指定,它選擇的辨別就是啟動使用者的辨別。換句話說,配置設定給伺服器程序的辨別與啟動它的用戶端程序的辨別相同。在這種情況下,如果 Bob 登入機器 A,并使用 CoCreateInstanceEx 連接配接機器 B 上的 Singleton 對象,而 Alice 也在機器 C 上如法炮制,就會啟動兩個不同的伺服器程序(至少在兩台不同的 WinStation 上),實際上使用戶端無法再用 Singleton 語義與共享的對象執行個體連接配接。

兩個測試場景之是以會産生大相徑庭的結果,其原因就是在一個場景(那個可以工作的場景)中,所有測試人員都使用隻為測試而設定的一個特殊帳戶以同一個人的身份登入。而在另一個場景中,測試人員都使用他們的普通使用者帳戶登入。當兩個或更多的用戶端程序具有相同辨別時,它們可以成功連接配接到配置為假定啟動使用者辨別的伺服器程序。但是,如果用戶端有不同的辨別,SCM 會使用多個伺服器程序(每個唯一用戶端辨別一個)分隔配置設定給不同對象執行個體的辨別。

COM的八個經驗和教訓COM的八個經驗和教訓

圖1 DCOMCNFG 中的使用者帳戶

找到問題以後,解決起來就很簡單了:配置 COM 伺服器,讓其使用特定的使用者帳戶而不是假定啟動使用者的辨別。完成這一任務的一種方式是在伺服器機器上運作 DCOMCNFG(Microsoft 的 DCOM 配置工具),并将“launching user ”更改為“This user”(請參見圖 1)。如果您喜歡通過程式設計方式進行更改(可能從安裝程式着手),請在主機系統資料庫的 HKEY_CLASSES_ROOT\AppID 部分的 COM 伺服器項中添加 RunAs 值(請參見圖 2)。

COM的八個經驗和教訓COM的八個經驗和教訓

圖 2 添加 RunAs 值到系統資料庫中

您還需要使用 LsaStorePrivateData 将 RunAs 帳戶的密碼存儲為 LSA 密鑰,并使用 LsaAddAccountRights 確定帳戶擁有“Logon as batch job”的權限。(有關具體操作的示例,請參見 Platform SDK 中的 DCOMPERM 示例。請特别注意名為 SetRunAsPassword 和 SetAccountRights 的函數。)

DCOM 不适于防火牆

關于 DCOM 特性和功能的一個常見問題是:“它能跨 Internet 工作嗎?” DCOM 能夠很好地跨 Internet 工作,隻要将它配置為使用 TCP 或者 UDP,并且通過授予任何人啟動和通路權限,可将伺服器配置為允許匿名方法調用。畢竟,Internet 是一個巨大的 IP 網絡。但沖突的是,如果您将一個現有的 DCOM 應用程式(在公司的内部網絡或 intranet 中工作得很好)改為跨 Internet 工作,它很有可能失敗得很慘。可能是什麼原因呢?防火牆。

DCOM 生來與防火牆的關系就如油與水的關系。原因之一是 COM 的 SCM 使用端口 135 與其他機器上的 SCM 通信。防火牆限制了它可以使用的端口和協定,可能會拒絕通過端口 135 傳入的通信量。但更大的問題在于,為了避免與使用套接字、管道和其他 IPC 機制的應用程式沖突,DCOM 沒有固定使用特定範圍的端口,相反,它在運作時才選擇所使用的端口。預設情況下,它可以使用從 1,024 到 65,535 範圍内的任何端口。

允許 DCOM 應用程式通過防火牆的一種方式是,為 DCOM 要使用的協定打開端口 135 和端口 1,024-65,535。(預設情況下,Windows NT 4.0 是 UDP 協定,Windows 2000 是 TCP 協定。)但是,這比移除所有防火牆好不了多少。對此,您公司的 IT 人員可能要發表意見了。

另一種更安全和更現實的解決方案是,限制 DCOM 使用的端口範圍,并隻為 DCOM 通信量打開一組小範圍端口。根據實踐原則,您應該為每個伺服器程序配置設定一個端口,将連接配接導出到遠端 COM 用戶端(不是每個接口指針一個端口或每個對象一個端口,而是每個伺服器程序一個)。将 DCOM 配置為使用 TCP 而不是 UDP 是一個好方法,特别是在伺服器對其用戶端執行回調時。

DCOM 用于遠端連接配接的端口範圍和所用的協定可通過系統資料庫進行配置。在 Windows 2000 和 Windows NT 4.0 Service Pack 4 或更高版本上,您可以用 DCOMCNFG 應用這些配置更改。以下是将 DCOM 配置為通過防火牆工作的辦法。

COM的八個經驗和教訓COM的八個經驗和教訓

圖3 選擇協定

在伺服器(在防火牆後寄存遠端對象的機器)上,将 DCOM 配置為使用 TCP 作為其所選協定,如圖 3 中所示。

在伺服器上,限制 DCOM 将使用的端口範圍。記住為每個伺服器程序至少配置設定一個端口。圖 4 中的示例将 DCOM 限制為端口 8,192 到 8,195。

打開您在步驟 2 中選擇的端口,使 TCP 通信量能夠通過防火牆。同時打開端口 135。

COM的八個經驗和教訓COM的八個經驗和教訓

圖4 選擇端口

執行這些步驟,DCOM 就可以很好地跨防火牆工作了。如果您願意,SP4 和更高版本還可讓您為單獨的 COM 伺服器指定終結點。更多資訊,請閱讀 Michael Nelson 關于 DCOM 和防火牆的優秀論文,該論文可在 MSDN Online 站點上找到(請參見 http://msdn.microsoft.com/library/enus/dndcom/html/msdn_dcomfirewall.asp)。 

還應注意的是,通過在伺服器上安裝 Internet 資訊服務 (IIS),并使用 COM Internet 服務 (CIS) 通過端口 80 路由 DCOM 通信量,SP4 和更高版本的使用者還可以使用 CIS 來提供與防火牆相容的 DCOM。有關該主題的更多資訊,請參閱 http://msdn.microsoft.com/library/en-us/dndcom/html/cis.asp。 

使用線程或異步調用來避免 DCOM 逾時設定太長

總是有人問我當 DCOM 無法完成遠端執行個體化請求或方法調用時出現的逾時設定太長的問題。典型的場景如下:用戶端調用 CoCreateInstanceEx 來執行個體化遠端機器上的一個對象,但是這台機器臨時離線了。在 Windows NT 4.0 上,激活請求不會立即失敗,DCOM 可能會花上一分鐘或更長時間來傳回失敗的 HRESULT。DCOM 還可能花費很長時間,使指向已不再存在或其主機已離線的遠端對象的方法調用失敗。如果可能,開發人員應該如何避免這些較長的逾時設定呢?

要回答這個問題,幾句話是講不清楚的。DCOM 高度依賴于基礎網絡協定和 RPC 子系統。并沒有什麼神奇的設定可讓您限制 DCOM 逾時設定的持續時間。但是,我經常使用兩種技巧來避免較長逾時設定的負作用。 

在 Windows 2000 中,當調用在 COM 信道中挂起時,您可以使用異步方法調用來釋放調用線程。(有關異步方法調用的介紹,請參 MSDN Magazine 2000 年 4 月刊的“Windows 2000: Asynchronous Method Calls Eliminate the Wait for COM Clients and Servers Alike”。如果異步調用在合理時間内沒有傳回,您可以通過調用用于初始化調用的調用對象上的 ICancelMethodCalls::Cancel 來取消它。 

Windows NT 4.0 不支援異步方法調用,甚至在 Windows 2000 中也不支援異步激活請求。怎麼解決呢?從背景線程調用遠端對象(或是執行個體化該對象的請求)。使主線程在事件對象上阻塞,并指定逾時設定值以反映您願意等待的時間長度。當調用傳回時,讓背景線程來設定事件。假設主線程使用 WaitForSingleObject 阻塞,當 WaitForSingleObject 傳回時,傳回值可以告訴您,傳回是因為方法調用或激活請求傳回,還是因為您在 WaitForSingleObject 調用中指定的逾時設定到期。您不能在 Windows NT 4.0 中取消挂起調用,但是至少主線程可以自由地執行自己的任務。 

下面的代碼示範了基于 Windows NT 4.0 的用戶端如何才能從背景線程調用對象。

//

// Placing a Method Call from a Background Thread

/

HANDLE g_hEvent;

IStream* g_pStream;

// Thread A

g_hEvent = CreateEvent (NULL, FALSE, FALSE, NULL);

CoMarshalInterThreadInterfaceInStream (IID_IFoo, pFoo, &g_pStream);

DWORD dwThreadID;

CreateThread (NULL, 0, ThreadFunc, NULL, 0, &dwThreadID);

DWORD dw = WaitForSingleObject (g_hEvent, 5000);

if (dw == WAIT_TIMEOUT) {

 // Call timed out

}

else {

 // Call completed

}

...

// Thread B

IFoo* pFoo;

CoGetInterfaceAndReleaseStream (g_pStream, IID_IFoo, (void**) &pFoo);

pFoo->Bar (); // Make the call!

SetEvent (g_hEvent);

CloseHandle (g_hEvent);

在此示例中,線程 A 封送了一個 IFoo 接口指針,并啟動線程 B。線程 B 取消封送了該接口指針,并調用 IFoo::Bar。無論調用傳回所花費的時間有多長,線程 A 都不會阻塞超過 5 秒鐘,因為它在 WaitForSingleObject 的第二個參數中傳遞的是 5,000 (機關為微秒)。這并不是太好的辦法,但是如果“無論線上路的另一端發生什麼情況,線程 A 都不會挂起”這一點很重要的話,忍受這種麻煩也算值得。

共享對象并不容易

從我收到的郵件和在會議上被問到的問題判斷,困擾許多 COM 程式員的一個問題是如何将兩個或更多的用戶端與一個對象執行個體連接配接。要回答這個問題,寫出長篇大論(或是一本小冊子)都很容易,但其實隻要說明與現有對象的連接配接既不容易也不自動化,就足夠了。COM 提供了大量建立對象的方式,包括很受歡迎的 CoCreateInstance(Ex) 函數。但是 COM 缺乏一種通用的命名服務,允許使用名稱或 GUID 來辨別對象執行個體。而且它沒有提供内置的方式來建立對象,然後将它辨別為調用的目标以檢索接口指針。

這是不是意味着将多個用戶端與單一對象執行個體連接配接就不可能了呢?當然不是。實作這一點有五種方式。在這些資源連結中,您可以找到更多資訊甚至是示例代碼,來指導您的操作。請注意,這些技術從一般意義上講不能互換;通常,環境因素會決定哪種方式(如果有)适用于手邊的任務: Singleton 對象 Singleton 對象就是隻執行個體化一次的對象。可能會有 10 個用戶端調用 CoCreateInstance 來“建立”Singleton 對象,但實際上,它們都是接收指向同一對象的接口指針。ATL COM 類可通過在其類的聲明中添加 DECLARE_CLASSFACTORY_SINGLETON 語句,來轉換為 Singleton。

檔案名字對象 如果一個對象實作了 IpersistFile,并在運作中對象表 (ROT) 中使用檔案名字對象(它封裝了傳遞給對象的 IPersistFile::Load 方法的檔案名稱)注冊了自己,那麼用戶端就可以使用檔案名字對象連接配接對象的現有執行個體了。實際上,檔案名字對象允許使用檔案名稱來命名對象執行個體,對象可在這些檔案名稱中存儲它們的持久性資料。它們甚至能夠跨機器工作。

CoMarshalInterface 和 CoUnmarshalInterface 儲存接口指針的 COM 用戶端可以與其他用戶端共享這些接口指針,隻要它們願意封送指針。COM 為願意将接口指針封送給同一程序中其他線程的線程提供了優化(請參見教訓 2),但是如果用戶端線程屬于其他程序,CoMarshalInterface 和 CoUnmarshalInterface 就是實作接口共享的關鍵途徑了。

【轉載】https://www.cnblogs.com/mazhenyu/archive/2011/11/10/2244829.html

繼續閱讀