Warning
STA 執行緒中非同步/等待與阻擋通話的死鎖風險:在受管程式碼(.NET)中,STA 訊息迴圈對 COM 呼叫調度至關重要。 以 Task.Wait()、Task.Result、Thread.Sleep() 或 ManualResetEvent.WaitOne() 封鎖 STA 執行緒,會使 COM 回撥和跨 Apartment 呼叫無法完成,因而造成死結。
// ❌ DEADLOCK — blocks the STA message loop
[STAThread]
static void Main()
{
var result = GetDataAsync().Result; // Deadlock: awaited continuation
// can't post back to this STA thread
}
// ✅ Correct — use async entry point or pump messages
[STAThread]
static async Task Main()
{
var result = await GetDataAsync(); // continuation resumes on STA via SynchronizationContext
}
如果你無法使用 await,請使用 CoWaitForMultipleHandles (原生)或 dispatcher frame/message pump(管理型)來取代 STA 執行緒上的原始等待原語。
使用單個線程 Apartment (Apartment 模型進程) 提供訊息型範例,以處理同時執行的多個物件。 它可讓您撰寫更有效率的程式碼,方法是在某個執行緒等待耗時作業完成時,讓另一個執行緒得以執行。
初始化為 Apartment 模型進程且擷取和分派視窗訊息的進程中的每個線程都是單個線程 Apartment 線程。 每個線程都位於它自己的 Apartment 內。 在 Apartment 中,介面指標可以傳遞而不進行封送處理,因此,單一線程 Apartment 線程中的所有對象都會直接通訊。
一組彼此相關且都在同一執行緒上執行的物件所形成的邏輯群組,因此必須同步執行,可能位於同一個單一執行緒 Apartment 執行緒上。 不過,Apartment 模式物件不能存在於多個執行緒上。 對其他執行緒中的物件所做的呼叫,必須在擁有該物件的執行緒脈絡中進行,因此當您透過 Proxy 進行呼叫時,分散式 COM 會自動為您切換執行緒。
進程間和線程間模型很類似。 當必須在相同進程中將介面指標傳遞至另一個 Apartment 中某個物件(在另一個線程上)時,您會使用相同的封送處理模型,讓不同進程中的物件用來跨進程界限傳遞指標。 藉由取得標準封送處理物件的指標,您可以如同在處理序之間那樣,跨執行緒界限(執行緒單元之間)封送處理介面指標。 (介面指標在不同 Apartment 之間傳遞時,必須先封送處理。)
單個線程 Apartment 的規則很簡單,但請務必仔細遵循這些規則:
- 每個物件都應該只存在於一個線程上(在單個線程 Apartment 內)。
- 初始化每個線程的 COM 連結庫。
- 在 Apartment 之間傳遞時,請封送處理所有指向物件的指標。
- 每個單一執行緒 Apartment 都必須有訊息迴圈,才能處理來自其他處理程序以及同一處理程序內其他 Apartment 的呼叫。 沒有物件的單執行緒 Apartment(僅限用戶端)也需要訊息迴圈,以派送某些應用程式所使用的廣播訊息。
- DLL 型或進程內物件不會呼叫 COM 初始化函式;相反地,他們會在登錄中的 InprocServer32 機碼下,使用 ThreadingModel 具名值來註冊其線程模型。 具 Apartment 感知能力的物件也必須小心撰寫 DLL 入口點。 對於處理序內伺服器的執行緒處理,有一些特殊考量。 如需詳細資訊,請參閱 In-Process 伺服器線程問題。
雖然多個物件可以存在於單一線程上,但任何 Apartment 模型物件都不能存在於多個線程上。
客戶端進程或跨進程伺服器的每個線程都必須呼叫 CoInitialize,或呼叫 CoInitializeEx,併為 dwCoInit 參數指定COINIT_APARTMENTTHREADED。 主要 Apartment 是第一個呼叫 CoInitializeEx 的執行緒。 如需處理序內伺服器的相關資訊,請參閱 處理序內伺服器執行緒問題。
對物件的所有呼叫都必須在其執行緒上進行(在其公寓內)。 禁止直接從另一個線程呼叫 物件;以這個自由線程方式使用 物件可能會導致應用程式發生問題。 這條規則意味著,所有指向物件的指標在 Apartment 之間傳遞時都必須進行封送處理。 COM 為此提供下列兩個函式:
- CoMarshalInterThreadInterfaceInStream 會將介面封送處理至串流物件中,並將該物件傳回給呼叫端。
- CoGetInterfaceAndReleaseStream 會從資料流物件還原介面指標,並將其釋放。
這些函式會包裝對 CoMarshalInterface 和 CoUnmarshalInterface 函式的呼叫,而函式需要使用 MSHCTX_INPROC 旗標。
一般而言,封送處理是由 COM 自動完成。 例如,將介面指標當做方法呼叫中的參數傳遞至另一個 Apartment 中的物件時,或呼叫 CoCreateInstance時,COM 會自動執行封送處理。 不過,在某些特殊情況下,應用程式寫入器會在 Apartment 之間傳遞介面指標,而不使用一般 COM 機制,寫入器必須手動處理封送處理。
如果處理程序中的某個 Apartment(Apartment 1)具有介面指標,而另一個 Apartment(Apartment 2)需要使用該介面指標,則 Apartment 1 必須呼叫 CoMarshalInterThreadInterfaceInStream 來封送該介面。 此函式所建立的數據流是安全線程的,而且必須儲存在 Apartment 2 可存取的變數中。 Apartment 2 必須將此數據流傳遞至 CoGetInterfaceAndReleaseStream 將介面取消封存,並取得可存取介面的 Proxy 指標。 主要 Apartment 必須保持運作,直到用戶端完成所有 COM 工作為止(因為某些同進程物件會載入主要 Apartment 中,如 In-Process 伺服器線程問題中所述)。 以這種方式在線程之間傳遞一個對象之後,將介面指標當做參數傳遞很容易。 如此一來,分散式 COM 會針對應用程式執行封送處理和線程切換。
若要處理來自其他處理程序以及同一處理程序內其他 Apartment 的呼叫,每個單一執行緒 Apartment 都必須有訊息迴圈。 這表示線程的工作函式必須有 GetMessage/DispatchMessage 迴圈。 如果使用其他同步處理基本類型在線程之間通訊,則 MsgWaitForMultipleObjects 函式可用來等候訊息和線程同步處理事件。 此函式的說明文件中包含這類組合迴圈的範例。
COM 會在每個單一執行緒 Apartment 中,使用 Windows 視窗類別 "OleMainThreadWndClass" 建立隱藏視窗。 對物件的呼叫會以傳送至這個隱藏視窗的視窗訊息形式被接收。 當物件的 Apartment 擷取並分派訊息時,隱藏的視窗將會收到它。 視窗程序接著會呼叫 對象的對應介面方法。
當多個用戶端呼叫某個物件時,這些呼叫會進入訊息佇列,而該物件每當其執行緒單元擷取並分派訊息時,就會接收一次呼叫。 由於呼叫會由 COM 同步處理,而且呼叫一律由屬於物件 Apartment 的線程傳遞,因此對象的介面實作不需要提供同步處理。 單個線程 Apartment 可以實作 IMessageFilter,以允許他們在必要時取消呼叫或接收視窗訊息。
如果其某個介面方法實作會擷取並分派訊息,或對另一個執行緒進行 ORPC 呼叫,因而導致另一個呼叫被傳遞至該物件(來自同一個 Apartment),則該物件可重入。 OLE 不會防止相同線程重新進入,但有助於提供線程安全性。 這與視窗程序在處理訊息時,如果擷取並分派訊息而可能再次進入的情況完全相同。 不過,呼叫另一部單個線程 Apartment 伺服器的跨進程單個線程 Apartment 伺服器,將允許重新進入第一部伺服器。
相關主題