首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

COM 開發實戰:來自一線經驗的八大血淚教訓

     編輯:關於VC++

前言

在日常工作中,我閱讀過無數位開發人員撰寫的 COM 程式碼。既驚嘆於許多富有創意的 COM 使用方式,也對錯誤一次次重演感到無力。這些錯誤多集中在執行緒與安全性兩大領域——恰好也是 COM 文件最缺乏、卻最容易絆倒開發者的地方。

本文不提供可直接複製的程式碼,而是透過八位程式設計師的真實經歷,揭示實作基於 COM 應用程式的正確與錯誤做法,助您避開前人踩過的坑。

教訓一:永遠呼叫 CoInitialize(Ex)

一位知名硬體公司工程師遭遇詭異問題:複雜的 COM 應用程式在測試中運作正常,但在關閉時對 Release 的呼叫竟然死結。根因竟是未在所有客戶端執行緒中呼叫 CoInitialize 或 CoInitializeEx。

現代 COM 鐵律:每個使用 COM 的執行緒必須先初始化 COM。CoInitialize(Ex) 會將執行緒置入單元(Apartment)並初始化必要的執行緒狀態。缺少此呼叫有時不會立即顯現,直到對 Release 的呼叫一去不回才暴露。雖然微軟知識庫曾稱 MTA 執行緒可略過此呼叫,但除非你百分之百確定不會受影響,否則永遠呼叫它無害且必要。

教訓二:切勿在執行緒間直接傳遞原始介面指標

一個 10 萬行的分散式應用程式,從背景執行緒呼叫多台機器上的 COM 物件,調用要么消失、要么無故失敗,甚至導致同機器上其他支援 COM 的應用程式(如畫圖)凍結。

違反了 COM 並發基本規則:若一執行緒要與另一執行緒共享介面指標,必須先封送(Marshal)該指標。未封送直接傳遞 32 位元位址會繞過 COM 並發機制,若發送與接收執行緒位於不同單元,會出現調用失敗、返回 RPC_E_WRONG_THREAD_ERROR 等詭異行為。

Windows NT 4.0+ 提供 CoMarshalInterThreadInterfaceInStream 與 CoGetInterfaceAndReleaseStream 輕鬆完成跨執行緒封送:

// 執行緒 A 封送
CoMarshalInterThreadInterfaceInStream(IID_IFoo, pFoo, &pStream);

// 執行緒 B 取消封送
IFoo* pFoo;
CoGetInterfaceAndReleaseStream(pStream, IID_IFoo, (void**)&pFoo);

也可使用全域介面表(GIT,Windows NT 4.0 SP4+),從 GIT 擷取的指標會自動封送。

教訓三:STA 執行緒必須有訊息迴圈

上述專案另一致命缺陷:MFC 輔助執行緒呼叫 CoInitialize 或 AfxOleInit 進入 STA,創建 COM 物件並封送指標給其他執行緒。從創建執行緒呼叫物件順利,但從其他執行緒呼叫永不返回。

原因:STA 執行緒若無訊息迴圈,針對該 STA 物件的跨單元呼叫將永遠滯留在訊息佇列中。COM 在 STA 建立隱藏視窗,跨單元呼叫轉為訊息投遞至該視窗佇列,執行緒必須泵送訊息才能處理。MFC 輔助執行緒預設無訊息迴圈,故不適合寄宿需接收跨單元呼叫的物件。

解決之道:

  • 為 STA 執行緒加上訊息迴圈:while (GetMessage(&msg,0,0,0)) DispatchMessage(&msg);
  • 或將 COM 執行緒移至 MTA(或 Windows 2000 的 NTA),無訊息佇列依賴。

教訓四:單元模型物件必須保護共享資料

標記 ThreadingModel=Apartment 的進程內物件,實例會建立在呼叫者的 STA 中。若用戶端有五個 STA 執行緒各自 CoCreateInstance,將產生五個並行執行的實例。若這些實例共享資料(全域變數、靜態成員、函數靜態變數),除非所有存取皆為讀取,否則必須同步化存取,否則會產生難以重現的時序錯誤。

許多開發者誤以為 Apartment 模型自動保證線程安全,實則不然——它只是承諾你會以線程安全方式存取共享資料。即使 static int nCallCount++; 這樣看似無害的語句,在 STA 中也會出錯,正確寫法:

static int nCallCount = 0;
InterlockedIncrement(&nCallCount); // 或使用臨界區、互斥體

教訓五:謹慎選擇啟動使用者身分

分散式應用中,客戶端連接遠端 Singleton 物件。測試時兩種場景結果截然不同:一種正常連接同一實例,另一種卻產生多個伺服器進程導致無法共享實例。差別在於測試人員登入身分不同。

DCOM SCM 在遠端啟動進程時,預設使用啟動使用者的身分。若 Bob 與 Alice 分別從不同機器啟動,SCM 會建立兩個不同身分的伺服器進程,破壞 Singleton 語義。

解決:配置 COM 伺服器使用特定使用者帳號而非啟動使用者身分。在伺服器機器執行 DCOMCNFG,將「Launching User」改為「This User」;或在登錄 HKEY_CLASSES_ROOT\AppID 加入 RunAs 值,並用 LsaStorePrivateData 儲存密碼、LsaAddAccountRights 授予「Logon as batch job」權限。

DCOMCNFG 使用者帳號設定 登錄加入 RunAs 值

教訓六:DCOM 與防火牆天生不合

DCOM 能跨 Internet 運作(配置 TCP/UDP、授予匿名權限),但現有內網 DCOM 應用移至 Internet 多半慘烈失敗——防火牆是主因。

  • SCM 使用埠 135 通訊,防火牆可能封鎖。
  • DCOM 執行時動態選擇埠(預設 1024~65535),無固定埠範圍,防火牆難以配置。

較安全方案:限制 DCOM 使用埠範圍,每個伺服器進程分配一個埠,配置使用 TCP(尤其需回呼時)。Windows 2000/NT 4.0 SP4+ 可用 DCOMCNFG 設定:

  1. 伺服器設定 DCOM 使用 TCP(圖 3)。
  2. 限制埠範圍,如 8192~8195(圖 4),記得開放埠 135。
  3. 防火牆開放這些 TCP 埠。

選擇協定 選擇埠範圍

SP4+ 也可用 COM Internet Services (CIS) 透過埠 80 路由 DCOM 流量。

教訓七:用執行緒或非同步調用避開 DCOM 超時過長

遠端機器離線時,CoCreateInstanceEx 或方法調用在 Windows NT 4.0 上可能等待一分鐘才失敗。無單一設定能限制 DCOM 超時,但有兩招緩解:

  • Windows 2000:使用非同步方法調用釋放調用執行緒,超時可呼叫 ICancelMethodCalls::Cancel 取消。
  • Windows NT 4.0 / 非同步激活不支援時:從背景執行緒發起調用,主執行緒在事件物件上 WaitForSingleObject 並指定可接受超時。背景執行緒完成時觸發事件,主執行緒超時即可繼續執行,不被封鎖。
// 背景執行緒調用範例 (NT 4.0)
HANDLE g_hEvent; IStream* g_pStream;
// 執行緒 A
g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
CoMarshalInterThreadInterfaceInStream(IID_IFoo, pFoo, &g_pStream);
CreateThread(NULL, 0, ThreadFunc, NULL, 0, &dwThreadID);
DWORD dw = WaitForSingleObject(g_hEvent, 5000); // 5 秒超時
if (dw == WAIT_TIMEOUT) { /* 超時處理 */ }
// 執行緒 B
IFoo* pFoo;
CoGetInterfaceAndReleaseStream(g_pStream, IID_IFoo, (void**)&pFoo);
pFoo->Bar(); SetEvent(g_hEvent); CloseHandle(g_hEvent);

教訓八:共享物件並不容易

如何讓多個客戶端連接同一物件實例?COM 提供建立物件的多種方式,但缺乏通用命名服務,無內建方式建立物件後標記為共享目標。五種實現共享的途徑(環境決定適用性):

  1. Singleton 物件:只實例化一次,CoCreateInstance 皆返回同一指標。ATL 可用 DECLARE_CLASSFACTORY_SINGLETON。
  2. 檔案名字物件:實作 IPersistFile 並在 ROT 中以檔案名註冊,客戶端可用檔案名連接現有實例,甚至跨機器。
  3. CoMarshalInterface / CoUnmarshalInterface:保存介面指標的客戶端可封送指標共享給其他進程客戶端。
  4. 自訂類別物件:實作替代 IClassFactory 的自訂啟動介面,提供檢索既有實例的方法。
  5. 自訂名字物件:用 C 風格字串命名實例,透過 MkParseDisplayName 取得連接的名字物件(NT 4.0 不支援跨機器)。

共享前請自問:共享真的是必須的嗎?若方法調用從外部資料源(資料庫、硬體、全域變數)取得資料,為何不給每個客戶端一個實例,各自存取資料源?同步化資料源存取即可,這正是 MTS 應用程式的運作模式,已證明是極具吸引力的程式設計模型。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved