COM線程模型3

套間生成規則

  線程在進行大多數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套間正確工作)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章