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