In-Process 伺服器執行緒問題

Warning

從 .NET 程式碼進行跨 Apartment 封送處理:當 .NET 應用程式建立其 ThreadingModel 與呼叫端所在 Apartment 不符的處理序內 COM 物件時,COM 會在另一個 Apartment 中建立該物件,並傳回代理物件。 透過此代理呼叫方法會產生編組開銷,若目標 Apartment 的執行緒被封鎖,還可能導致死結。

常見故障模式:

// A .NET console app defaults to MTA (no explicit CoInitializeEx).
// Creating an Apartment-threaded COM object forces COM to spin up
// a hidden STA thread. If that STA thread has no message pump,
// calls that require marshaling back to it will hang.

// Fix: If your COM object requires STA, mark Main with [STAThread]:
[STAThread]
static void Main(string[] args)
{
    var comObj = new MyCOMObject(); // Created in the STA — no proxy needed
}

對於進行中 COM 伺服器的 .NET 使用者的最佳實務:

  • 請檢查 HKCR\CLSID\{...}\InprocServer32 下該物件的 ThreadingModel 登錄值
  • 如果要使用 apartment-threaded 物件,請在進入點使用 [STAThread](WinForms/WPF 會自動這麼做)
  • 切勿在 STA 執行緒上呼叫 Thread.Join()Task.Wait()——請使用 await,或以 Dispatcher.PushFrame() 處理訊息

進行中的伺服器不會呼叫 CoInitializeCoInitializeExOleInitialize 來標記其執行緒模型。 對於有執行緒感知的 DLL 或進行中物件,你需要在登錄檔中設定執行緒模型。 當你不指定執行緒模型時,預設模型是每個程序使用單一執行緒。 要指定模型,你可以在登錄檔的 InprocServer32 鍵上加入 ThreadingModel 值。

支援類別物件實例化的 DLL 必須實作並匯出 DllGetClassObjectDllCanUnloadNow 這兩個函式。 當用戶端需要 DLL 支援的類別實例時,呼叫 CoGetClassObject (直接或透過呼叫 CoCreateInstance)會呼叫 DllGetClassObject ,取得指向其類別物件的指標,當該物件實作於 DLL 中時。 因此,DllGetClassObject 應該能提供多個類別物件或單一的執行緒安全物件(基本上就是在內部引用計數上使用 InterlockedIncrement/InterlockedDecrement)。

顧名思義, DllCanUnloadNow 是用來判斷實作它的 DLL 是否正在使用,若未被使用,呼叫者便能安全卸載。 從任何執行緒呼叫 CoFreeUnusedLibraries 時,都會經過主公寓的執行緒來呼叫 DllCanUnloadNow

與其他伺服器一樣,進行中伺服器可以是單執行緒、公寓執行緒或自由執行緒。 這些伺服器可被任何 OLE 用戶端使用,無論該用戶端採用何種執行緒模式。

用戶端與處理程序內物件之間,允許所有執行緒模型互通的組合。 客戶端與使用不同執行緒模型的進行中物件之間的互動,與客戶端與非進程伺服器之間的互動完全相同。 對於進行中伺服器,當客戶端與進行中伺服器的執行模型不同時,COM 必須在客戶端與物件之間插入。

當一個支援單執行緒模型的進程物件被多個用戶端執行緒同時呼叫時,COM 不能允許用戶端執行緒直接存取該物件的介面(該物件本身並非設計用於此類存取)。 相反地,COM 必須確保呼叫是同步的,且僅由建立物件的用戶端執行緒執行。 因此,COM 在用戶端的主公寓建立物件,並要求其他用戶端公寓透過代理存取該物件。

當客戶端中的自由執行緒公寓(多執行緒公寓模型)建立公寓執行緒的進行中伺服器時,COM 會在客戶端啟動單執行緒公寓模型的「主機」執行緒。 這個主機執行緒會建立物件,介面指標則會被組織回用戶端的自由執行緒公寓。 同樣地,當公寓模型客戶端中的單執行緒公寓建立自由執行緒的進程中伺服器時,COM 會啟動一個自由執行緒的主機執行緒(多執行緒公寓,物件將在此建立後再編回客戶端單執行緒公寓)。

Note

一般來說,如果你在進行中的伺服器上設計自訂介面,也應該提供其編制代碼,讓 COM 能在客戶公寓間管理介面。

 

COM 會要求必須從建立這些物件的同一個用戶端公寓進行存取,藉此協助保護對單一執行緒 DLL 所提供之物件的存取。 此外,所有 DLL 入口點(例如 DllGetClassObjectDllCanUnloadNow)和全域資料都應該由同一個 apartment 存取。 COM 會在客戶端的主公寓中建立此類物件,讓主公寓能直接存取物件的指標。 來自其他 Apartment 的呼叫會使用執行緒間封送處理,從 Proxy 傳到主 Apartment 中的 Stub,再傳到該物件。 這使得 COM 能夠同步對物件的呼叫。 執行緒間呼叫速度緩慢,因此建議將這些伺服器重寫以支援多個公寓。

如同單執行緒的進程中伺服器,公寓模型 DLL 提供的物件必須由該物件建立的同一用戶端公寓存取。 然而,此伺服器提供的物件可在用戶端的多個 Apartment 中建立,因此伺服器必須實作其入口點(如 DllGetClassObjectDllCanUnloadNow)以供多執行緒使用。 例如,如果一個客戶端的兩個 apartment 嘗試同時建立兩個進行中的物件實例,兩個 apartment 可以同時呼叫 DllGetClassObjectDllCanUnloadNow 必須寫入,使得 DLL 在程式碼仍在執行時不會卸載。

如果 DLL 只提供一個類別工廠實例來建立所有物件,類別工廠實作也必須設計成多執行緒使用,因為會被多個用戶端存取。 如果 DLL 每次呼叫 DllGetClassObject 都會建立一個新的類別工廠實例,那麼類別工廠不必是執行緒安全的。

由類別工廠建立的物件不必是執行緒安全的。 一旦由執行緒建立,物件總是透過該執行緒被存取,且所有對物件的呼叫都會由 COM 同步。 建立此物件的客戶端公寓模型會直接指向該物件。 與建立該物件的公寓不同的那些客戶公寓,必須透過代理來存取該物件。 這些代理是在用戶端管理其公寓間介面時產生的。

當進行中的 DLL ThreadingModel 值被設定為「Both」時,該 DLL 提供的物件可以直接(無需代理)在單執行緒或多執行緒的用戶端中建立並使用。 不過,它只能直接在創作的公寓內使用。 要將該物件交給其他公寓,必須經過管理。 DLL 物件必須實作自己的同步功能,且可同時被多個用戶端公寓存取。

為了加速自由執行緒存取進程中 DLL 物件的效能,COM 提供了 CoCreateFreeThreadedMarshaler 函式。 此函式會建立一個自由執行緒的封送物件,可與進行中的伺服器物件聚合。 當同一程序中的客戶端公寓需要存取另一個公寓中的物件時,聚合自由執行緒的 marshaler 能讓客戶端直接指向伺服器物件,而非代理指標,當用戶端將物件介面管理到另一個公寓時。 用戶端不需要做任何同步。 這只在同一流程內運作;標準的編組用於指向傳送到其他程序的物件。

Important

現代替代方案CoCreateFreeThreadedMarshaler針對 Windows 8.1+ 的新程式碼,建議使用 RoGetAgileReference 及其介面IAgileReference。 這些系統提供了一種更安全的方式,讓物件參考在公寓間傳遞,避免了自由執行緒 Marshaler 的陷阱(該系統完全繞過公寓保護,且能掩蓋執行錯誤)。 對於 WinRT 物件,敏捷性是預設的——除非 WinRT 物件明確選擇退出,否則會自動支援 IAgileObject

由進程中 DLL 提供的僅支援自由執行緒的物件稱為自由執行緒物件。 它實作自己的同步功能,並可同時被多個用戶端執行緒存取。 此伺服器不在執行緒間編排介面,因此此伺服器只能由客戶端中的多執行緒公寓直接建立與使用(無需代理)。 建立這份服務的單執行緒公寓會透過代理伺服器存取。

跨 Apartment 存取介面

選擇執行緒模型

多執行緒公寓

程序、執行緒與公寓

單執行緒與多執行緒通訊

單一執行緒公寓