COM Threading Part 1

<<COM Threading Part 1>>

Hongjiang 時間: 2000-09-08

首先,COMApartment概念是爲了讓COM的開發和使用都容易才引入Apartment概念的。但是關於Apartment概念詳細闡述的比較好的資料在國內可能比較少,所以有很多人對其理解上存在一些疑問。這很正常,我當初爲理解它也花了2周時間,參考了不少資料。

其次,marshal的問題。marshal主要是COM用來在進程間以及計算機間進行COM調用時用的,即Proxy/stub模型。但是在進程內部有時也需要進行marshal,在下面詳細闡述時,我會說明marshal相關的問題。

要注意的是,有一些COM的基本概念在這兒我不會說明,我想在這兒的討論COM的人因該知道。還有,因爲我用的是日語鍵盤,敲中文不方便,本文中有錯別字還請多包涵。上面講了不少廢話,下面言歸正傳。

1 COM Apartment的背景

 大家都知道,在一個多線程的操作系統中,在線程中對一個多個線程公用的變量進行操作時,線程的同步是必須的。這個變量可以是一個簡單的Integer類型,也可以是一個class或是一個COM對象。 對一個簡單的Integer變量來說,線程的同步很簡單,每次對它進行操作的時候用Mutex等進行同步。 對於一個classCOM對象來說,你也可以採用對簡單變量一樣的方法,但更好的方法是在其內部進行線程同步,這樣便於使用。也就是說,你在實現這個classCOM對象時,就要寫線程同步代碼。如果classCOM對象內部實現了線程同步,那麼它就是Thread-safe的。

 現在的問題是,並不是每個人在寫COM對象都保證它是Thread-safe的。 如果沒有COMApartment 那麼我們對所有開發的COM對象都要貼上一個標籤指明它是不是Thread-safe,這樣使用這個COM對象的人才知道他如果要在多線程方式下使用這個COM對象時是不是要進行線程同步。 COMRuntime爲了使大家不用在自己開發的COM對象上貼上這麼一個標籤,而開發人員可以在沒有寫線程同步代碼的情況下照樣可以用不是Thread-safeCOM對象,引入了Apartment

2 COM Apartment的概念
         爲了解決上面所講的Thread-safe問題,引入了COM Apartment概念。但到底COM Apartment是什麼? 大家考慮一下這個問題: 如果我寫了一個不是Thread-safeCOM對象,把它交給一個使用者,而且告述他是Thread-safe的。那麼如果使用者在多線程環境下用我寫的這個COM對象就不會寫線程同步代碼,會出現什麼情況? 答案是明顯的,執行結果會有問題。 COMRuntime爲了使使用者在開發者沒有告述他COM對象的Thread-safe問題的情況下也能在各個線程模式下安全使用,要求一個COM對象能夠告述COMRuntime環境它能在什麼線程模式下被安全使用, 同時,使用者在使用一個COM對象之前,也必須告述COMRuntime環境他將在什麼線程模式下使用這個COM對象,如果兩者的線程模式不一樣,那麼COMRuntime環境就會介入,爲它們完成線程的同步問題。COM Apartment就是COMRuntime環境對COM ClientCOM對象的線程模式的包裝(實際上是在TLS裏面加上了線程模式的標誌) 在開發一個COM對象時開發者必須指定這個COM對象的線程模式(這一點大家應該都已經知道了) COM對象的線程模式會在它註冊時寫入系統的註冊表。 使用者在每個使用COM對象的線程中必須首先調用CoInitializeCoInitializeEx等來告述COM Runtime環境將在那種線程模式下使用COM對象。

3 COM對象的建立與調用

 在一個COM對象被建立時,COM Runtime會根據它在註冊表中指定的線程模式來建立它的Apartment。關於COM對象的各種線程模式我不想在這兒多說,到處都能找到有關的說明。

關於Apartment的模式有以下幾種:

Primary Single-Threaded Apartment: 這種Apartment在一個進程中只會有一個, 而且只處在第一次建立它的線程中。它對應Single線程模型。

(PrimarySTA)Single-Threaded Apartment: 這種Apartment在一個進程中會有多個。它對應partment線程模型。(STA)

Multi-Threaded Apartment: 這種Apartment在一個進程中只會有一個,  它對應Free線程模型。

(MTA)Any : 這種Apartment在一個進程中可能有多個,也可能只有一個, 它對應Both線程模型。(STA/MTA)

Thread-Neutral Apartment: 這時在COM+中出現的, 它一直執行在COM Client的進程之外的獨立的進程之中。對應Neutral線程模型。(TNA)

COM Client在指定它所使用的線程模型時在調用CoInitializeEx是第二個參數用Single-Threaded Apartment(COINIT_APARTMENTTHREADED)Multi-Threaded Apartment(COINIT_MULTITHREADED)來指明進入的是那種Apartment

COM Runtime在建立COM對象時根據它在註冊表中指定的線程模式將COM對象建立相應的Apartment之中, 然後根據COM Client所使用的線程模式, COM調用採取不同的動作。下面我想討論一下不同COM ClientCOM對象的線程模式COM Runtime採取的動作,但對於Primary Single-Threaded Apartment,因爲這種模式效率不好,基本已經不用,所以對它不進行討論。

1)COM對象爲STAClient線程初始化爲COINIT_APARTMENTTHREADED COM Runtime將建立STA COM對象,Client的線程進入這個STA並直接得到COM對象指針。

2)COM對象爲STAClient線程初始化爲COINIT_MULTITHREADED 如果這個COM對象沒有被建立過,COM Runtime將爲這個COM對象建立一個新的線程並在這個新線程中建立STA COM對象, Client線程進入的是MTA,得到的將是新的線程中這個COM對象的被marshal的指針。如果這個COM對象被建立過,COM Client線程得到的將是已經建立的STA線程中COM對象的被marshal的指針。

3)COM對象爲MTAClient線程初始化爲COINIT_APARTMENTTHREADED 如果這個COM對象沒有被建立過,COM Runtime將爲這個COM對象建立一個新的線程並在這個新線程中建立MTA COM對象, COM Client線程進入STA,得到的將是新的線程中這個COM對象的被marshal的指針。如果這個COM對象被建立過,Client線程得到的將是已經建立的MTA線程中COM對象的被marshal的指針。

4)COM對象爲MTACOM ClientCOINIT_MULTITHREADED 如果這個COM對象沒有被建立過,COM Runtime將建立MTA COM對象。COM Client線程進入MTA並直接得到COM對象的指針。如果這個COM對象被初始化過, Client線程進入已經建立的MTA並直接得到COM對象的指針。

5)COM對象爲AnyCOM ClientCOINIT_APARTMENTTHREADED 如果這個COM對象沒有被初始化過,COM Runtime將建立STA COM對象,Client線程進入STA,直接得到COM對象指針。

6)COM對象爲AnyCOM ClientCOINIT_MULTITHREADED 如果這個COM對象沒有被初始化過,COM Runtime將建立MTA COM對象,Client線程進入這個MTA並直接得到COM對象的指針。如果這個COM對象被初始化過,Client線程進入已經建立的MTA並直接得到COM對象的指針。

7)COM對象,爲TNA 這個從COM+開始的線程模式比較特殊,無論COM Client用那種方式初始化,它都只存在於Client進程之外(DllHostExe)。而且Client用那種方式初始化都可以'直接'進入TNA 請注意,直接是打了引號的,其實,Client進入TNA時是通過marshal的,只是這個marshal的作用稍微有點不同,這在以後說明。

4 關於marshal

 COMmarshal分爲三種: 進程內的marshal 同一計算機中進程間的marshal,以及不同計算機間的marshal 進程內和進程間的marshal是通過Local RPC完成的,計算機間的marshal通過DCE RPC來完成。 進程間和計算機間的marshal是必須的,進程內marshal是在不同Apartment之間進行方法調用和傳遞對象Interface時發生。

在同一Apartment內的調用用不着marshal 舉個例子來說, 一個處於MTA中的Client線程,想要調用一個處於STA中的對象時, COM Runtime會走進來, 對這個調用進行marshal。爲什麼要marshal? 因爲MTA本身說明了現在是一個多線程的環境, STA中的對象不是Thread-safe的,那麼對這個不是Thread-safe的對象的調用必須要序列化(排隊)

COM Runtime爲了保證不是Thread-safe的對象的調用序列化, 必須要截獲對該對象的調用, 然後進行排隊。 marshal就是起這個作用。 實際上,COM Runtime會截獲MTA中的線程對STA對象的調用(通過Proxy),將這個調用通過消息傳遞方式傳遞給STA對象的stub 在完成調用後由stub將結果傳回Proxy 對於對象的Interface 也是同樣的道理。

 對於COM+Thread-Neutral Apartment比較特殊, 它是一直需要marshal的, 一個方面是因爲它處於不同的進程中。另外一個重要原因是, COM+提供了一系列新的功能, Object Pooling Object Construct String等。 COM+必須要截獲ClientCOM對象的調用才能完成將COM對象從緩衝池中取出以及放回緩衝池等的操作。

 那麼marshal是自動還是手工完成的呢? 方法調用是自動完成的。對象的Interface的傳遞,一般情況下,  是自動完成的,比如你通過調用CoCreateInstanceExCoGetClassObject等得到的對象Interface,以及通過方法調用傳遞的對象Interface。但是有些情況下必須手工marshal 還是形象寫,舉個例子: 比如我有一個COM Server 它監視一個工控裝置的信號, 如果信號有異常,它要通知客戶端,讓客戶端進行報警動作。爲了實現這個功能, 客戶端和我的COM Server通過IConnectionPoint完成事件觸發機制。爲了提高性能, COM Server的主線程接受客戶端通過IConnectionPointAdvise傳來的Interface指針, 並將之放到一個Interface指針表裏, 主線程運行在STA中。 另外建立了一個運行於MTA中的線程專門用來監視信號,如果信號異常,它將調用Interface指針表裏所有Interface的方法來通知客戶端。 現在如果理解COM的機制的人看到我這個實現方法就知道這裏面需要對Interface指針進行手工marshal 爲什麼, 客戶端通過IConnectionPoint傳給我的COM Server主線程的Interface指針是自動進行了marshal 但是, 由於我的COM Server的專門用來監視信號的線程運行在和主線程不同的Apartment之中, 對這個線程來說, 這些Interface指針是沒有經過marshal的, 在調用是就會出現RPC_E_WRONG_THREAD錯誤。 要解決這個問題,有兩個辦法,

 1) 讓我的主線程也運行在MTA中。這種方法簡單。 2) 手工marshal 在主線程中得到客戶端的Interface指針後, 調用CoMarshalInterThreadInterfaceInStream 得到一個IStream的指針,讓後將它放到IStream的指針表裏, 監視信號的線程要通知客戶端時,從IStream的指針表取得IStream指針, 然後調用CoGetInterfaceAndReleaseStream得到marshal後的客戶端Interface指針。 這種方法有個缺點, 就是一旦調用CoGetInterfaceAndReleaseStream後這個IStream指針就被釋放掉了,下一次就取不到了。 更好的解決方法是採用GIT(globalinterface table) 主線程將它得到的Interface指針放到GIT 監視信號的線程從GIT中取到的Interface指針是正確marshal了的。 GIT是一個COM對象, 有三個方法提供
Interface
指針的存取,使用也很簡單,這兒就不多說了,具體請參照幫助。

 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章