COM線程模型-套間(轉)

COM線程模型-套間
來源: http://blog.csdn.net/crybird/archive/2008/10/11/3057067.aspx
查找了好多資料,終於對套件這一概念有一點心得,趕緊記錄下來。
首先,只要遵守COM規範,不用COM庫也能編寫COM程序,那相當於自己實現用到的COM庫函數。本篇COM如果單獨出現,指COM庫。
1   進程、線程回顧
《WINDOWS核心編程》對進程和線程有深入解釋,一個程序運行起來,需要一個進程作爲容器。進程管理所有系統資源、內存、線程等等。線程是CPU的調度單位,有自己的棧和寄存器狀態。程序最初創建的線程叫主線程,主線程可以創建子線程,子線程還可以創建子線程。
不同進程之間是無法直接通信的,因爲它們在虛擬內存中的地址不一樣。但操作系統通過LPC機制,可以完成不同進程之間的通信。
 
COM在進程間通信的方法是本地過程調用(LPC),因爲操作系統知道各個進程的確切邏輯地址,所以操作系統可以完成這一點。不同進程間傳遞的參數需要調整,LPC技術可以完成普通數據的直接拷貝(甚至包括自定義類和指針),但對於接口參數,COM實現了IMarshal接口以調整。
爲了可以用同樣的方式和進程外、遠程組件通信,客戶端不直接和組件通信,而是和代理/存根通信,代理/存根是(而且必須是) DLL形式,能完成參數調整和LPC調用。代理存根不用自己寫,系統會自動產生。
注:接口的調整,包括列集和散集兩種marshal/unmarshal。
2   COM線程模型
2.1 分清模型與實現
    看過《Inside C++ Object Model》(中文名:深入C++對象模型;侯捷譯)的人都知道,C++對象模型有三種,各家編譯器都選擇其中效率最高的一種實現出來。另外兩種就留在了理論世界,實現出來沒有太大意義。提這個的原因,就是爲了弄清楚這一點:COM線程模型只是理論構想,是一種抽象的數學模型,還要COM庫通過各種手段實現出來,才能爲我們使用。
2.2 套間的由來
最開始的COM庫,支持的使用組件的唯一模式是single-thread-per-process模式。這樣就避免了多線程的同步,而且組件執行的線程肯定是創建它的線程。
然而組件對象真正的執行環境很複雜。COM組件的執行環境有兩種:單線程環境Single-Thread,多線程環境Multi-Thread。單線程要考慮執行線程是否是創建組件的線程;多線程還要考慮併發、同步、死鎖、競爭等問題。無論哪種環境,都要編寫大量的代碼以使COM組件對象正確的運行。
爲了使程序員減輕痛苦,COM庫決心提供一套機制來幫助程序員。如果我們都遵從這套機制,只要付出較少的勞動,就可以讓組件對象和COM庫一起完成工作。COM庫這套機制的核心技術就是“套間技術”。
2.3 COM的多線程模型
2.3.1 COM庫的規定
關於多線程問題方面,COM庫做出瞭如下規則(不是COM標準,是COM庫爲了簡化多線程編程中對組件的調用而制定的):
1.  COM庫提供兩種套間,單線程套間和多線程套間,COM組件的編寫者最好提供對應的屬性(後面會提到),COM組件的使用者要在套間裏創建和調用組件。
2.  COM庫對所有的調用進行參數調整(如果需要),不管是對進程內服務器的調用,還是對進程外服務器的調用。
3.  線程內調用、跨線程調用、跨進程調用都用統一的方式。需要用代理的會用代理。
 
如此COM規定了COM庫、組件編寫者、組件使用者三方合作關係。COM庫進行協調關係,會根據組件的能力,在不同環境(套間)中創建和調用組件;編寫者要說明組件可以生存的環境;調用者查詢接口,合理調用。
2.3.2 單線程套間STA
Single-threaded Apartments,一個套間只關聯一個線程,COM庫保證對象只能由這個線程訪問(通過對象的接口指針調用其方法),其他線程不得直接訪問這個對象(可以間接訪問,但最終還是由這個線程訪問)。
COM庫實現了所有調用的同步,因爲只有關聯線程能訪問COM對象。如果有N個調用同時併發,N-1個調用處於阻塞狀態。對象的狀態(也就是對象的成員變量的值)肯定是正確變化的,不會出現線程訪問衝突而導致對象狀態錯誤。
注意:這只是要求、希望、協議,實際是否做到是由COM決定的。這個模型很像Windows提供的窗口消息運行機制,因此這個線程模型非常適合於擁有界面的組件,像ActiveX控件、OLE文檔服務器等,都應該使用STA的套間。
2.3.3 多線程套間MTA
Multithreaded Apartments,一個套間可以對應多個線程,COM對象可以被多個線程併發訪問。所以這個對象的作者必須在自己的代碼中實現線程保護、同步工作,保證可以正確改變自己的狀態。
這對於作爲業務邏輯組件或幹後臺服務的組件非常適合。因爲作爲一個分佈式的服務器,同一時間可能有幾千條服務請求到達,如果排隊進行調用,那麼將是不能想象的。
注意:這也只是一個要求、希望、協議而已。
2.3.4 COM+新增NA
COM+爲了進一步簡化多線程編程,引入了中立線程套間概念。
NA/TNA/NTA,Neutral Apartment/Thread Neutral Apartment / Neutral Threaded Apartment。這種套間只和對象相關聯,沒有關聯的線程,因此任何線程都可以直接訪問裏面的對象,不存在STA的還是MTA的。
2.4 到底什麼是套間
根據《COM技術內幕》的觀點,COM沒有定義自己新的線程模型,而是直接利用了Win32線程,或者說對其做了改造、包裝。線程間的同步也是直接用的Win32 APIs。
《COM本質論》設這樣定義的:套間定義了一組對象的邏輯組合,這些對象共享一組併發性和衝入限制。每個COM對象都屬於某一個套間,一個套間可以包含多個COM對象。
MSDN上解釋說,可以把進程中的組件對象想象爲分成了很多組,每一組就是一個套間。屬於這個套間的線程,可以直接調用組件,不屬於這個套間的線程,要通過代理才能調用組件。
最直接的說,COM庫爲了實現簡化多線程編程的構想,提出了套間概念。套間是一個邏輯上的概念,它把Win32裏的線程、組件等,按照一定的規則結合在一起,並且以此提供了一種模式,用於多線程併發訪問COM組件方面。可以把套間看作COM對象的管理者,它通過調度,切換COM對象的執行環境,保證COM對象的多線程調用正常運行。COM和線程不是包含關係,而是對應和關聯關係。
3   第一方COM庫:模型的實現
3.1 單線程套間STA
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);這句代碼創建了一個STA,然後套間把當前的線程和自己關聯在一起,線程被標記爲套間線程,只有這個線程能直接調用COM對象。
在創建套間的時候,COM創建了一個隱藏的窗口。關聯線程對組件的調用直接通過接口指針調用方法;其他線程對套間裏的對象的調用,都轉變成對那個隱藏窗口發送消息,然後由這個隱藏窗口的消息處理函數來實際調用組件對象的方法。編寫組件代碼的時候,只需調用DispatchMessage即可將方法調用的消息和普通的消息區分開來(通過隱藏窗口的消息處理函數)。
由於窗口消息的處理是異步的,所以所有的調用都是依次進行的,不必考慮同步的問題。只要調用的時候,參數進行合理調整即可(COM庫會做到這一點)。但是對於全局變量和靜態變量,組件編寫者還是要費心的。
 
一個STA只關聯一個線程, single-thread-per-process模式只是STA的一個特例。使用這種模式的線程叫套間線程(Apartment Thread)。
3.2 多線程套間MTA
CoInitializeEx(NULL, COINIT_MULTITHREADED);第一次如此調用的時候,會創建一個MTA,然後套間把當前線程和自己關聯在一起,線程被標記爲自由線程。以後第二個線程再調用(在同一進程中)的時候,這個MTA會把第二個線程也關聯在一起,並標記爲自由線程。一個MTA可以關聯多個線程。
所有的關聯線程都可以調用套間中的組件。這就涉及到同步問題,需要組件編寫者解決。
一個MTA可以關聯一個或多個線程,這種模式下,COM組件自己要考慮同步問題。使用這種模式的這些線程叫做自由線程(Free Thread) 。
3.3 總結
一個進程可以有0個、1個或多個STA,還可以有0個或1個MTA。
 
一個線程,進入(或創建)套間後,不能改變套間模式;但可以退出套間,然後以另外的模式再進入(或創建)另一個套間。
 
在一個進程中,主套間是第一個被初始化的。在單線程的進程裏,這是唯一的套間。調用參數在套間之間被調整,COM庫通過消息機制處理同步。
如果你設計多個線程作爲自由線程,所有的自由線程在同一個單獨的套間中,參數被直接(不被列集)傳遞給這個套間的任何線程,而且你要處理所有的同步。
在既有自由線程又有套間線程的進程裏,所有自由線程在一個套間裏,而其他套間都是單線程套間。而進程是包含一個多線程套間和N個單線程套間的容器。
 
COM的線程模型爲客戶端和服務器提供了這樣一種機制:讓不同的線程協同工作。不同進程內,不同線程之間的對象調用也是被支持的。以調用者的角度來看,所有對進程外對象的調用都是一致的,而不管它在怎樣的線程模型。以被調用者的角度來看,不管調用者的線程模型如何,所獲得的調用都是一致的。
 
客戶端和進程外對象之間的交互也很直接,即使它們使用了不同的線程模型,因爲它們屬於不同的進程。COM介入了客戶端和服務器之間,通過標準的列集和RPC,並提供了跨線程操作的代碼。
4   第二方COM組件的調用者
4.1 各種調用
4.1.1 同一線程中的調用
同步問題:不需要,調用者和組件在同一線程中,自然同步。
調整問題:不需要,COM庫不需要任何介入,參數也不需要調整,組件也不必線程安全。
調用地點:當前線程
這是最簡單的情況。
4.1.2 套間線程之間的調用
同步問題:COM庫對調用進行同步。
調整問題:不管兩個套間是否在同一進程,都需要調整。某些情況下,需要手動調整。
調用地點: 對象所在套間線程。
4.1.3 自由線程之間的調用
同步問題:COM不進行同步,組件自己同步。
調整問題:同一進程不調整,不同進程要調整。
調用地點:客戶線程。
4.1.4 自由線程調用套間線程的組件
同步問題:COM庫對調用進行同步。
調整問題:不管兩個套間是否在同一進程,都需要調整。某些情況下,需要手動調整。
調用地點:套間線程
4.1.5 套間線程調用自由線程的組件
同步問題:COM不進行同步,組件自己同步。
調整問題:需要調整,同一進程,COM會優化調整。
調用地點:客戶線程。
4.2 手工調整
如果通過COM方法,所有的參數都由COM庫進行調整。有時候需要程序員手工對接口指針進行列集marshal和散集unmarshal,那就是在跨越套間邊界,但沒有通過COM庫進行通信的時候。更明確的說,不通過COM接口函數,通過我們自己寫的函數跨套間傳遞接口指針的時候。
 
情況一:跨套間傳遞接口指針。
情況二:類廠在另外的套間中,創建類實例,並傳回給客戶端的接口指針。
 
列集函數:CoMarshalInterThreadInterfaceInStream
散集函數:CoGetInterfaceAndReleaseStream 
5   第三方COM組件的編寫者
組件將在哪種類型的套間中執行,是編寫者決定的。對於進程外組件,要調用CoInitializeEx並指定參數,以顯示確定套間類型。對於進程內的服務器來說,因爲客戶端已經調用CoInitializeEx產生套間了,爲了允許進程內的服務器可以控制它們的套間類型,COM允許每個組件有自己不同的線程模型,並記錄在註冊表中。
HKEY_CLASSES_ROOT/CLSID/.../InprocServer32 鍵值ThreadingModel
5.1 線程模型屬性
組件編寫者可以實現:同一個組件,既可以在STA中運行,也可以在MTA中運行,還可以在兩中環境中同時存在。可以說組件有一種屬性說明可以在哪種環境中生存,屬性名叫做“線程模型”(相當於“隱藏”)也未嘗不可。COM+裏真正引入了這個屬性,並叫做ThreadModel。這個屬性可以有如下取值:
1.  Main Thread Apartment
2.  Single Thread Apartment (Apartment)
3.  Free Thread Apartment (Free)
4.  Any Apartment (Both)
5.  Neutral Apartment (N/A)
5.2 對象在哪個套間創建
下表中第一列爲套間種類,第一行爲對象線程模型屬性。那麼,結果就是在這樣的套間中創建這樣的組件,組件在什麼地方。在必要的時候,會創建一個代理,就是表中的宿主。
 

 

未指定

Apartment

Free

Both

Neutral

單線程

(非主)

STA

當前套間

MTA

當前套間

NA

單線程

(主線程)

當前套間

當前套間

MTA

當前套間

NA

多線程

 

STA

宿主STA

MTA

MTA

NA

Neutral

單線程

主線程套間

宿主STA

(本線程)

MTA

NA

NA

Neutral

多線程

主線程套間

宿主STA

MTA

NA

NA

5.3 屬性的選擇
原則是根據組件的功能選擇:
如果組件做I/O,首選Free,因爲可以相應其他客戶端調用。
如果組件和用戶交互,首選Apartment,保持消息依次調用。
COM+首選N/A。
如果沒有定義,COM庫默認爲是Main Thread Apartment。
Apartment簡單,Free強大但要自己同步。
6   鳴謝
《COM技術內幕》《COM本質論》《深入解析ATL》
6.1 MSDN2008
在MSDN 2008中相關文檔的位置:
Win32和COM開發
  -組件開發
    -組件對象模型
      -SDK文檔
        -COM
          -COM Fundamentals
            -Guide-Processes, Threads, and Apartments
Win32和COM開發
  -組件開發
    -COM+
      -SDK文檔
        -COM+(組件服務)
          -COM+開發瀏覽
            -COM+ Contexts and Threading Models
6.2 COM線程模型
http://hi.baidu.com/zhangqiuxi/blog/item/ca7aa52b0311b4fbe6cd401e.html
6.3 理解 COM 套間
http://www.vckbase.com/document/viewdoc/?id=1597
6.4 泛說"COM線程模型"
http://blog.csdn.net/guogangj/archive/2007/09/06/1774280.aspx
6.5 附泛說一文
 
COM線程模型在COM相關的基礎知識中應該算是難點,難的原因可能有這些:
 
1、需要對COM其它基礎知識有較深的瞭解,因爲這個論題幾乎涉及到了COM所有其它的基礎知識。
 
2、學習者得非常瞭解Win32本身的線程模型,因爲在Windows中COM的線程模型在建立在Win32線程模型的基礎上的。
 
3、COM線程模型所引用的概念十分抽象,不好理解。
 
如果你還沒有掌握 1,2 所提到的知識點,你可以馬上找一些書籍,迅速補充這些知識,如果你已經掌握了這些知識,那就給你的想象力上點油,輕鬆點。
 
 
 
6.5.1 開始想象
 
術語
 
公寓(Apartment)有的譯文譯作"套間"。這個術語抽象的是COM對象的生存空間,你還真的可以想象成公寓,線程就是住在公寓裏的人。
 
單線程公寓(Single-Threaded Apartment STA) 這種房間是供有錢人住的單人間,設備齊全,服務周到。
 
多線程公寓(Multithreaded Apartment MTA) 住在這種房間裏的人條件就差多了,那麼多人就擠在一個大房間裏頭,可是他們自強不息。個個健壯得不得了。
 
 
 
然後思考
 
單線程公寓與多線程公寓的本質差別有哪些?
 
如果另一個人要和住在單線程公寓的人通信,不能直接去找他,哪怕你也住在高貴的單人間。但你可以打電話。提醒一下,電話每次只能同時與一個人說話(他們還沒有用到電話會議之類的服務)。住在多線程公寓的人他們的房間有個大窗子,如果住在單人間(單線程公寓)的人想與他們通信,來窗口說就行,而且這個窗子比你想的要大,可以同時讓很多人對話。同一房間裏的人不用說了,他們可以直通話。
 
 
 
6.5.2 回到現實
 
術語
 
1、公寓,如果從來就不用考慮線程同步的問題,就用不着這個概念了,可是 COM 決定支持強大的多線程,於是引入了這個概念,公寓決定了線程與外界通信的方式。每一個與COM對象打交道的線程必須先決定要進入哪種公寓。
 
2、單線程公寓,這種公寓本身只能包含一個線程,通過調用CoInitialize(NULL)進入。它有着與窗口類似的運作方式,回想一下窗口的運行方式:消息泵不斷的從消息隊列提取消息,然後調度給相應的窗口函數。這樣做的好處是,窗口函數永遠不會重入,也就是說永遠不會有同步的問題。單線程公寓也用了同樣的運作方式(所以該公寓中的線程的主函數必須有一個消息循環):對該公寓中線程所擁有的COM對象的調用被隊列化,只有當一個調用結束後,另個調用纔會開始。那麼組件對象的代碼也是永遠不會重入。
 
3、多線程公寓,這種公寓可以包含任意多的線程(具體數目由操作系統決定)。一個進程裏頭只能包含一個這種公寓,所有調用 CoInitializeEx(NULL, COINIT_MULTITHEADED)都會進入這個公寓。對該公寓中線程所擁有的COM對象的調用是直接的(先不考慮跨進程的情況),包括本公寓中的線程與其它的STA線程。
 
 
 
然後思考
 
單線程公寓與多線程公寓 的本質差別有哪些?
 
單線程公寓實現同步,有很多COM庫的干預,包括將外部的調用轉化成窗口消息,然後那個特別的隱藏窗口的窗口函數把窗口消息轉化成COM對象的函數調用。這樣的模型可以減小開發組件的難度,可是,卻犧牲了效率。多線程公寓把實現同步任務全部交給了組件自己,所以在這種公寓中生存的COM對象必須足夠健壯,考慮各種同步問題,不至於多個線程在調用對象的成員函數時會打架。
 
 
 
6.5.3 弄清它們的關係
 
弄清公寓,線程,對象的關係是很重要的,你弄清了嗎?如果你沒有弄清,那上面的這些也一定也是看得懵懵懂懂。公寓是這裏面最大的單位,它是線程的容器。如果調用CoInitialize(0),COM庫會創建一個STA(注意,是"創建"),你的線程將屬於這個公寓,並且是這個公寓的唯一成員。如果 CoInitializeEx(NULL, COINIT_MULTITHEADED),而且是第一個要求進入MTA的線程,COM庫會創建一個MTA,其它後面調用 CoInitializeEx(NULL, COINIT_MULTITHEADED)的線程會直接進入(注意,我用的"進入")已有MTA。本來線程是一個運行的實體,不會分配資源,可是在 COM的線程模型裏一個對象與創建它的線程是緊密相關的,稱對象歸屬於某個線程,至於這種歸屬關係是在COM庫內怎麼管理,我們先不去管它,以後我們把線程A創建的對象說成是線程A的對象就行了(有一個例外,得說說,有一種Single 類型的COM對象,這種對象基實就是COM在提出線程模型前的產物,這種對象總是歸屬於主STA線程,即第一個調用CoInitialize(0)的線程。)
 
 

在這一部分我將講解COM提出的各個類型的線程模型,並說明COM運行時期庫是如何實現它們的。

本文講解COM提出的各個類型的線程模型,再說明COM運行時期庫是如何實現它們的.

線程模型是一種數學模型,專門針對多線程編程而提供的算法,但也僅是算法,不是實現。本文講解COM提出的各個類型的線程模型,再說明COM運行時期庫是如何實現它們的,就像說明Windows是如何實現線程這個數學模型的一樣,最後指明一下跨套間調用和各種類型套間編寫的要求以幫助理解。希望讀者對於Windows操作系統的線程這個概念相當熟悉,對何謂“線程安全的”亦非常瞭解。

  COM線程模型

  COM提供的線程模型共有三種:Single-Threaded Apartment(STA 單線程套間)、Multithreaded Apartment(MTA 多線程套間)和Neutral Apartment/Thread Neutral Apartment/Neutral Threaded Apartment(NA/TNA/NTA 中立線程套間,由COM+提供)。雖然它們的名字都含有套間這個詞,這只是COM運行時期庫(注意,不是COM規範,以下簡稱COM)使用套間技術來實現前面的三種線程模型,應注意套間和線程模型不是同一個概念。COM提供的套間共有三種,分別一一對應。而線程模型的存在就是線程規則的不同導致的,而所謂的線程規則就只有兩個:代碼是線程安全的或不安全的,即代碼訪問公共數據時會或不會發生訪問衝突。由於線程模型只是個模型,概念上的,因此可以違背它,不過就不能獲得COM提供的自動同步調用及兼容等好處了。

  STA 一個對象只能由一個線程訪問(通過對象的接口指針調用其方法),其他線程不得訪問這個對象,因此對於這個對象的所有調用都是同步了的,對象的狀態(也就是對象的成員變量的值)肯定是正確變化的,不會出現線程訪問衝突而導致對象狀態錯誤。其他線程要訪問這個對象,必須等待,直到那個唯一的線程空閒時才能調用對象。注意:這只是要求、希望、協議,實際是否做到是由COM決定的。如上所說,這個模型很像Windows提供的窗口消息運行機制,因此這個線程模型非常適合於擁有界面的組件,像ActiveX控件、OLE文檔服務器等,都應該使用STA的套間。

  MTA 一個對象可以被多個線程訪問,即這個對象的代碼在自己的方法中實現了線程保護,保證可以正確改變自己的狀態。這對於作爲業務邏輯組件或幹後臺服務的組件非常適合。因爲作爲一個分佈式的服務器,同一時間可能有幾千條服務請求到達,如果排隊進行調用,那麼將是不能想像的。注意:這也只是一個要求、希望、協議而已。


  NA 一個對象可以被任何線程訪問,與MTA不同的是任何線程,而且當跨套間訪問時(後面說明),它的調用費用(耗費的CPU時間及資源)要少得多。這準確的說都已經不能算是線程模型了,它是結合套間的具體實現而提出的要求,它和MTA不同的是COM的實現方式而已。
COM套間

  Apartment被翻譯成套間或是單元,是線程模型的一個實現者,就像在操作系統課程中講到的線程只是一個數學模型,而Windows的線程、進程是它(數學模型的線程、進程)的實現者。套間只是邏輯上的一個概念,實現時只是一個結構(由COM管理)而已,記錄着相關信息,如它的種類(只能是上面那三個,至少現在是),並由COM根據那個結構進行相應的處理。下面說明這三種套間的實現方式:

  STA套間 一個套間如果是STA,那麼那個套間有且只有一個線程和其關聯,有多個對象或沒有對象和其關聯,就像有多個線程和一個進程關聯一樣,也就是說套間那個結構和某個線程及多個對象之間有關係,關係具體是什麼由COM說得算,幸運的是COM正是按照上面的線程模型來定義互相之間關係的。根據上面的算法,很容易就知道只有這個線程可以訪問這個套間裏的對象。

  COM是通過在STA套間裏的線程中創建一個隱藏窗口,然後外界(這個套間外的線程)對這個對象的調用都轉變成對那個隱藏窗口發送消息,然後由這個隱藏窗口的消息處理函數來實際調用組件對象的方法來實現STA的規則的。之所以使用一個隱藏窗口是爲了方便組件代碼的編寫——只需調用DispatchMessage即可將方法調用的消息和普通的消息區分開來(通過隱藏窗口的消息處理函數)。外界對這個對象的調用都將轉變成對這個隱藏窗口的消息發送來實現同步。至於COM如何截獲外界對對象的調用,則是利於代理對象,後面再說明。

  值得注意的是,如果使用標準彙集法生成代理對象,則代理對象會根據是進程內還是進程外的跨套間調用,來決定具體操作。如果外界線程和STA線程在同一進程內,則代理對象將直接向STA線程中的隱藏窗口發送消息;如果不在同一進程內(包括遠程進程),代理對象將向RPC管理的一個線程池請求一個線程(RPC線程)來專門向另一進程中的STA線程的隱藏窗口發送消息,而不是代理對象直接發送消息,以防止外界線程由於網絡等不穩定因素而導致掛起。

  因爲COM利用消息機制來實現STA,因此STA套間裏的線程必須實現消息循環,否則COM將不能實現STA的要求。

  MTA套間 這種類型的套間可以和多個線程及多個或沒有對象相關聯。根據上面的MTA模型,可知只有這個套間裏的線程才能訪問這個套間裏的對象,和STA不同的只是可以多個線程同時訪問對象。

  外界(不屬於這個套間的線程)對這個套間裏的對象的調用將會導致調用線程(外界線程,也就是STA線程,因爲NA沒有線程)掛起,然後向RPC管理的一個線程池請求一個線程(RPC線程,並已經進入了這個MTA套間)以調用那個對象的方法。對象返回後,調用線程被喚醒,繼續運行。雖然可以讓STA線程直接調用對象(而不用像前述的掛起等待另一個線程來調用對象),但這是必須的,因爲可能會有回調問題,比如這個MTA線程又反過來回調外界線程中的組件對象(假設客戶本身也是一個組件對象,這正是連接點技術),如果異步回調將可能發生錯誤。

  反過來,MTA的線程訪問STA裏的對象時,COM將把調用轉換成對STA線程裏那個隱藏窗口的一個消息發送,返回後再由COM轉成結果返回給MTA的線程(如果使用標準彙集法生成標準代理對象,則發生的具體情況就如上面STA套間所述)。因此STA和MTA都是隻能由它們關聯的線程調用它們關聯的對象。而根據上面所說,當MTA調STA或STA調MTA,都會發生線程切換,也就是說一個線程掛起而換成執行另一個線程。這是相當大的消耗(需要從內核模式向用戶模式轉換,再倒轉好幾回),而NA就是針對這個設計的。

  NA套間 這種套間只和對象相關聯,沒有關聯的線程,因此任何線程都可以直接訪問裏面的對象,不存在STA的還是MTA的。

  外界(其實就是任何線程)對這個套間裏面的調用都不需要掛起等待,而是進入NA套間,直接調用對象的方法。NA套間是由COM+提供的,COM+中的每個對象都有一個環境和其相綁定,環境記錄了必要的信息,並監聽對對象的每一次調用,以保證當將對象的接口指針成員變量進行傳遞或回調時其操作的正確性(保證執行線程在正確的套間內,MTA線程就是通過將自己掛起以等待STA線程的消息處理完畢來保證的),從而避免了調用線程的掛起,因此這個代理(其實也就是環境的一部分)被稱作輕量級代理(相對於STA套間和MTA套間的重量級代理——需要掛起調用線程,發生線程切換)。

  這個輕量級代理並不是永遠都不發生線程切換。當NA對象裏有個對指向一個STA對象的指針的調用而調用線程不是那個STA對象關聯的線程時,調用將會轉成向被調用的STA對象的關聯線程發送消息,此時照樣會發生線程切換。同理,如果那個對象是MTA的,而調用線程是STA線程時,依舊發生線程切換。不過除此以外的大多數情況(即不在NA對象的方法中調用另一個套間對象的方法)都不會發生線程切換,即使出現上面的情況也只有必要(MTA調NA再調MTA就不用切換)才切換線程。

  根據上面所說,STA其實和MTA邏輯上是完全一樣的,只是一個是關聯一個線程,一個是關聯多個線程而已。但把它們分開是必要的,因爲線程安全就是針對是一個線程還是多個線程。而NA之所以不關聯線程是因爲它的目的是消除上面跨套間調用時產生的線程切換損耗,關聯線程沒有任何意義。

  COM強行規定(不遵守也沒轍,因爲全是COM實現套間的,根本沒有插手的餘地)一個進程可以擁有多個STA的套間,但只能擁有一個MTA套間和一個NA套間,我想這應該已經很容易理解了(要兩個MTA套間或NA套間幹甚?)。
套間生成規則

  線程在進行大多數COM操作之前,需要先調用CoInitialize或CoInitializeEx。調用CoInitialize告訴COM生成一個STA套間,並將當前的調用線程和這個套間相關聯。而調用CoInitializeEx( NULL, COINIT_MULTITHREADED );告訴COM檢查是否已經有了一個MTA套間,沒有則生成一個MTA套間,然後將那個套間和調用線程相關聯。接着在調用CoCreateInstance或CoGetClassObject等創建對象的函數時,創建的對象將以一個特定規則決定和哪個套間相關聯(後敘)。這樣完成後,就完成了線程、對象和套間的關聯(或綁定)。

  前面提到的決定對象去向的規則如下。

  當是進程內組件時,根據註冊表項<CLSID>/InprocServer32/ThreadingModel和線程的不同,列於下表:

 

創建線程關聯的套間種類 ThreadingModel鍵值 組件對象最後所在套間
STA Apartment 創建線程的套間
STA Free 進程內的MTA套間
STA Both 創建線程的套間
STA ""或Single 進程內的主STA套間
STA Neutral 進程內的NA套間
MTA Apartment 新建的一個STA套間
MTA Free 進程內的MTA套間
MTA Both 進程內的MTA套間
MTA ""或Single 進程內的主STA套間
MTA Neutral 進程內的NA套間

 



  進程內的主STA套間是進程中第一個調用CoInitialize的線程所關聯的套間(即進程中的第一個STA套間)。後面說明爲什麼還來個進程內的主STA套間。

  當是進程外組件時,由主函數調用CoInitializeEx或CoInitialize指定組件所在套間,與上面的相同,CoInitialize代表STA,CoInitializeEx( NULL, COINIT_MULTITHREADED );代表MTA,沒有NA。因爲NA是COM+提供的,而COM+服務只能提供給進程內服務器,因此只使用上面的註冊表項的規則決定DLL組件是否放進NA套間,而沒有提供類似CoInitializeEx( NULL, COINIT_NEUTRAL );來處理EXE組件。而且如果可以使用CoInitializeEx( NULL, COINIT_NEUTRAL );將導致調用線程和NA套間相關聯了,違背了NA的線程模型,這也是爲什麼ThreadingModel鍵在<CLSID>/InprocServer32鍵下。

  跨套間調用

  STA線程1創建了一個STA對象,得到接口指針IABCD*,接着它發起STA線程2,並且將IABCD*作爲線程參數傳入。在線程2中,調用IABCD::Abc()方法,成功或者失敗天註定。由於線程2所在的STA套間不同於線程1所在的STA套間,這樣線程2就跨套間調用另一個套間的對象了。按照前述的STA規則,IABCD::Abc()應該被轉成消息來發送,而如果如上面做法,可以,編譯通過,不過運行就不保證了。

  COM之所以能夠實現前面所說的那些規則(STA、MTA、NA),是因爲跨套間調用時,被調用的對象指針是指向一個代理對象,不是組件對象本身。而那個代理對象實現前述的那三個實現算法(轉成消息發送,線程切換等),而一般所說的代理/佔位對象(Proxy/Stub)等其實都只是指進行彙集工作的代碼(後述)。而按照上面直接通過線程參數傳入的指針是直接指向對象的,所以將不能實現STA規則,爲此COM提供瞭如下兩個函數(還有其他方式,如通過全局接口表GIT)來方便產生代理:CoMarshalInterface和CoUnmarshalInterface(如果在同一進程內的線程間傳遞接口指針,則可以通過這兩個函數來進一步簡化代碼的編寫:CoMarshalInterThreadInterfaceInStream和CoGetInterfaceAndReleaseStream)。
現在重寫上面代碼,線程1得到IABCD*後,調用CoMarshalInterface得到一個IStream*,然後將IStream*傳入線程2,在線程2中,調用CoUnmarshalInterface得到IABCD*,現在這個IABCD*就是指向代理對象的,而不是組件對象了。

  因此,前面所說過的所有線程模型的算法都是通過代理對象實現的。要跨套間時,使用CoMarshalInterface將代理對象的CLSID和其與組件對象建立聯繫的一些必要信息(如組件對象的接口指針)列集(Marshaling)到一個IStream*中,再通過任何線程間通信手段(如全局變量等)將IStream*傳到要使用的線程中,再用CoUnmarshalInterface散集(Unmarshaling)出接口以獲得指向代理對象的接口指針。因此之所以要獲得代理對象的指針是因爲想使用COM提供的線程模型(但在COM+中,這不是唯一的理由),如果不想使用大可不必這麼麻煩(不過後果自負),並沒有強制要求必須那麼做。

  當線程1和線程2都是MTA時,則可以像最開始說的那樣,直接傳遞IABCD*到線程2中,因爲MTA線程模型同意多個線程同時直接調用對象,線程1和線程2在同一個MTA套間中,而那個對象通過某種形式(如ThreadingModel = Free)向COM聲明瞭自己支持MTA線程模型。

  而當a.exe的線程1和b.exe的線程2都是MTA時,則依舊需要像上面那樣進行接口指針的彙集(列集→傳輸→散集這個過程)以得到指向代理而非對象的指針,即使線程1和線程2都是在MTA套間中,卻是在兩個不同的MTA套間中,因此是跨套間調用,需要彙集操作。

  彙集代碼

  前面已經說明了套間的規則都是通過對代理對象而非組件對象發起調用以截取對組件對象的調用由代理對象來實現的。代理對象要和組件對象交互,將方法參數傳遞給組件對象,需要使用到彙集技術,也就是列集→傳輸→散集這個過程。

  列集(Marshaling)指將信息以某種格式存爲流(IStream*)形式的操作;散集(Unmarshaling)則是列集的反操作,將信息從流形式中反還出來;傳輸則只是流形式的傳遞操作。

  這裏經常發生誤會。前面的CoMarshalInterface所做的列集,是將代理對象的CLSID及一些持久信息(用於初始化代理對象)格式化爲一種格式(網絡數據描述——Network Data Representation)後放到一個流對象中,可以通過網絡(或其他方式)將這個流對象傳遞到客戶機,由客戶通過CoUnmarshalInterface從傳來的流對象中反還出代理對象的CLSID和初始化用的一些持久信息,生成代理對象並使用持久信息初始化它以用於彙集操作。這就是發生誤會的地方——這裏的彙集操作不同於上面的彙集操作,其彙集的是接口方法的參數而不是什麼CLSID和一些初始化信息。

  因此CoMarshalInterface和CoUnmarshalInterface是用於彙集接口指針的,再準確點應該是用於生成代理對象的。代理對象應由讀者自己實現,用於彙集接口方法的參數。一般有兩種代理對象的實現方式:自定義彙集和標準彙集。

  對於自定義彙集,組件需實現IMarshal接口和一個代理組件(即完全實現真正組件所有接口的一個副本,實現了彙集方法參數及線程模型的規則,也必須實現IMarshal接口),並將這個代理組件在客戶機上註冊,以保證代理對象的正確生成。注意:如果參數中有接口指針,必須用CoMarshalInterface和CoUnmarshalInterface進行彙集,否則無法實現正確的線程模型,且代理組件是線程模型的實現者,這點組件必須自己保證(如發送消息等)。

  對於標準彙集,組件無需實現IMarshal接口及代理組件,代替的,組件則需要爲自己生成一個代理/佔位組件(Proxy/Stub),其由於可通過MIDL由IDL文件自動生成,效率高,代碼的正確性有保證,因而被鼓勵使用。COM提供了一個標準代理對象的實現,其通過聚合組件的代理/佔位組件以表現出其好像是組件的代理對象。與自定義彙集一樣,需要將這個代理/佔位組件在客戶機上註冊以保證代理對象的正確生成。

  至於這兩種彙集的具體工作機理,由於與本文無關,在此不表,這裏僅僅只爲消除代理對象和代理/佔位組件之間的混淆。

  注意:對於將運行於NA套間的組件,由於COM+的強制要求,其必須使用標準彙集進行代理對象的生成而不是自定義彙集(COM+運行時期庫重寫了標準代理對象來截獲對組件對象的調用和其自身的某些特殊處理——如保證NA套間正確工作)。

套間實現規則

  如前面所說,COM的套間機制要成功,必須服務器(組件)、客戶和COM運行時期庫三方面合力實現,其中有任何一方不按着規矩來,將不能實現套間機制的功能,不過這並不代表什麼錯誤,套間機制不能運作並不代表程序會崩潰,只是不能和其他COM應用兼容而已。

  比如:對象中的屬性1在設計的算法中肯定不會被兩個以上的線程寫入,只是會被多個線程同時讀出而已,因此不用同步,可以用MTA,但對象的屬性2卻可能被多個線程寫入,因此決定使用STA。從而在客戶端,通過前面說的CoMarshalInterface和CoUnmarshalInterface將對象指針傳到那個只會寫入對象的屬性1的線程,其實這時就可以直接將對象指針傳到這個線程,而不用想上面那樣麻煩(而且增加了效率),但是就破壞了COM的套間規矩了——兩個線程可以訪問對象,但對象在STA套間中。所以?!!什麼事都不會發生,因爲已經準確知道這個算法不會捅婁子(線程訪問衝突),即使破壞COM的規矩又怎樣?!而且組件仍可以和其他客戶兼容,因爲不按規矩來的是客戶,與組件無關。不過如果組件破壞規矩,那麼它將不能和每一個客戶兼容,但並不代表它和任何客戶都不兼容。這裏其實就是客戶和組件聯合起來欺騙了COM運行時期庫。

  上面的例子只是想幫助讀者加深對套間的理解,實際中應該儘量保持和COM規範的兼容性(但不兼容並不代表是錯誤的)。客戶要做的工作前面已經說過了(那兩個函數或全局接口表或其他只要正確的方式),下面說明組件應該做的工作。組件可以存在於四個套間中(多了一個主STA套間),所需工作分別如下:

  STA 當一個組件是STA時,它必須同步保護全局變量和靜態變量,即對全局變量和靜態變量的訪問應該用臨界段或其他同步手段保護,因爲操作全局和靜態變量的代碼可以被多個STA線程同時執行,所以那些代碼的地方要進行保護。比如對象計數(注意,不是引用計數),代表當前組件生成的對象個數,當減爲零時,組件被卸載。此變量一般被類廠對象使用,還好ATL和MFC已經幫我們實現了缺省類廠,這裏一般不用擔心,但自定義的全局或靜態變量得自己處理。

  主STA 與STA唯一的不同是這是傻瓜型的,連靜態和全局變量都可以不用線程保護,因爲所有不是安全訪問靜態和全局變量的對象都通過主線程(第一個調用CoInitialize的線程)的消息派送機制運行,因此不安全的訪問都被集中到了一個線程的調用中,因而調用被序列化了,也就實現了對靜態和全局變量的線程保護。至於爲什麼是主線程,因爲進程要使用STA,則一定會創建主線程,所以一定可以創建主STA。因此主STA並不是什麼第四種套間,只是一個STA套間,不過關聯的是主線程而已,由於它可以被用作保護靜態和全局變量而被單獨提出來說明。因此一個進程內也只有一個主STA套間。

  MTA 必須對組件中的每個成員和全局及靜態變量的訪問使用同步手段進行保護,還應考慮線程問題,即不是簡單地保護訪問即可,還應注意線程導致的錯誤的操作,最經典的就是IUnknown::Release()。

DWORD IUnknown::Release()
{
DWORD temp = InterlockedDecreament( &m_RefCount );
if( !temp ) // 不能用m_RefCount,原因請自己思考
delete this; // 因此不是隻要用原子訪問函數保護了m_RefCount的訪問就行了
return temp; // 前面對全局變量的保護也和此類似,要考慮線程問題
}

  如果讀者對自己多線程編程的技術沒有信心,建議最好不要編寫可以存在於MTA套間的組件,不過就不能獲得MTA的高性能了。

  在編寫MTA時還應該注意到線程親緣性(thread affinity)。沒有線程親緣性是指沒有任何線程範圍的成員變量,比如線程局部存儲(TLS)、窗口句柄等。也就是說在MTA中不能保存任何記錄着TLS內存的指針或窗口句柄,如果保存將沒有意義(比如A線程記錄的內存空間對B線程來說是無效的,因爲TLS構造了一個線程相關的內存空間,就像每個進程都有自己的私有空間)。而不幸地MFC在它的底層運作機制的實現中大量使用了TLS,如模塊線程狀態、線程狀態等。正是由於這個原因,MFC不能編寫在MTA中運行的組件。

  NA 由於可能會多個線程同時訪問NA套間的對象,因此和MTA一樣,其不能有線程親緣性並需要保護每個成員和全局及靜態變量。而關於NA的輕量級代理,是由COM+運行時期庫生成的,讀者完全不用操心(只需將那個組件的ThreadingModel鍵值賦值爲“Neutral”即可)。

  前面提到過有一種進程內組件的ThreadingModel鍵值可以被賦爲“Both”,這種組件很像NA,哪個套間都可能直接訪問它,但只是可能,而NA組件是可以,這點可以從前面的那個進程內組件所屬套間的規則表中看出。這種組件可以支持一種稱作自由線程彙集器(FTM——Free Threaded Marshaler)的技術,由於其與本文題目無關,在此不表。當Both的組件使用了自由線程彙集器時,除了滿足MTA的要求以外(上面所說的線程安全保護和沒有線程相關性),還要記錄傳進來的接口指針的中立形式(比如IStream*,通過CoMarshallInterface得到),以防止對客戶的回調問題。

  最後只是提醒一下,有3個STA套間,STA1、STA2和STA3。STA1用CoMarshallInterface得到的IStream*傳到STA2中通過CoUnmarshalInterface得到的代理和在STA3中同樣通過CoUnmarshalInterface得到的代理不同,不能混用。因爲當STA2和STA3調用在STA1的對象時,STA1如果回調(連接點技術就是一種回調)調用者,則STA2和STA3的代理能分別正確的指出需要讓哪個線程執行回調操作,即向哪個線程發送消息,因此不能混用。

 

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