COM組件以及套間

COM組件的特點:
1. COM組件是以WIN32動態鏈接庫(DLL)或可執行文件(EXE)形式發佈的可執行代碼組成。
2. COM組件是遵循COM規範編寫的   COM組件是一些小的二進制可執行文件
3. COM組件可以給應用程序、操作系統以及其他組件提供服務
4. 自定義的COM組件可以在運行時刻同其他組件連接起來構成某個應用程序
5. COM組件可以動態的插入或卸出應用
6. COM組件必須是動態鏈接的
7. COM組件必須隱藏(封裝)其內部實現細節
8. COM組件必須將其實現的語言隱藏
9. COM組件必須以二進制的形式發佈
10.COM組件必須可以在不妨礙已有用戶的情況下被升級
11.COM組件可以透明的在網絡上被重新分配位置
12.COM組件按照一種標準的方式來宣佈它們的存在
套間:
套間的提出是爲了組件在多線程環境下安全執行,因爲有跨線程調用同一個組件方法的狀況存在。若該組件接口是線程安全的,則無須套間,否則需要套間的協助,就如窗口過程函數一樣,
窗口過程本身並不是線程安全的,但是消息隊列的機制,保證了窗口過程總是在一個線程中執行,串行地處理消息。

要理解COM 和線程,必須要清楚套間(apartment)
套間只是一個應用程序中的邏輯容器,在這個套間之內,套間內所有COM對象遵守套間內線程規則,
比如 從一個套間內或者套間外線程訪問對象的方法或者屬性都需要遵守一樣的規則

套間的目的是什麼呢?
在任何包括多線程的程序中,都有可能存在多個線程同時訪問COM對象的問題,怎樣解決這種衝突呢?
那就是套間,COM套間出現的主要目的就是爲了線程安全,主要是指對象內屬相和方法,全局屬相和方法或者靜態屬相和方法除外,需要COM組件代碼進行保護。


套間包括下面節點規則:
1.每個COM對象只能屬於一個套間,屬於哪個套間,是在COM對象創建的時候就決定了。
2.一個COM線程(創建或者使用COM對象的線程)也只能屬於一個套間,跟COM對象一樣,一旦確定就不能改變
3.線程和屬於同一個套間的COM對象會遵守相同的調用規則,比如相同套間內的線程調用對象的方法,就會直接執行,與COM特性就沒有任何關係。
4.線程和COM組件如果是不同的套間,那他們各自的規則不一樣,方法的調用就會經過Marshal, 那就需要proxies和stubs.

除了保證線程安全,另外一個好處就是COM對象本身或者客戶端不需要關心對方的套間模型。

指定COM組件的套間模型
在DLL類型的COM 服務。 其中的coclass會指定線程模型,並且在註冊之後再註冊表裏就會有ThreadingModel鍵值,這個鍵值在InprocServer32下,如果用ATL編程,在加組件的時候,VS會有選項去設置。
COM組件的線程模型(Threading Model)共有四種:Single Apartment Both Free(可以通過修改註冊表直接改變這個組件的Treading Model)
Single: 當一個組件的線程模型被標識爲Single,說明進程中這個組件的實例 都必須在同一個套間線程中。(該套間線程就是進程創建的第一個STA套間線程)。無論在進程中創建多少個組件實例,接口調用都是在同一個線程中完成的。這 其中當然涉及到給隱藏窗口發送消息和等待,需要注意的是避免
線程間的死鎖問題。
Apartment: 當一個組件的線程模型被標識爲Apartment,當創建了一個組件,並把組件的某個接口傳遞給另一個線程時,在這個兩個線程中調用這個接口提供的方法,最終都是在同一個線程中完成的。說明該組件不是線程安全的,需要套間的協助。所處的套間線程必須是
COINIT_APARTMENTTHREADED。
Free: 說明組件是線程安全的,當發生2.2的狀況時,調用是在不同線程中完成的。但是所處套間必須是MTA(COINIT_MULTITHREADED)
Both: 說明組件是線程安全的,所處的套間MTA(COINIT_MULTITHREADED)和STA(COINIT_APARTMENTTHREADED)都可以。


指定線程的套間模型
一個COM線程(創建COM對象或者要使用COM對象的線程)必須進行初始化,初始化函數有CoInitializeEx()和CoInitialize(),CoInitializeEx是一個創建套間的過程,我們使用CoInitializeEx(NULL, COINIT_MULTITHREADED)後,會創建一個MTA套間。CoInitializeEx(NULL,
COINIT_APARTMENTTHREADED)創建個STA套間。一個進程可以包含多個STA,但只能有一個MTA。 一個STA只能包含一個線程,一個MTA可以包含多個線程。
缺省套間線程
當在創建組件時所處的線程(所創建的套間)與該組件的線程模型不匹配。這種狀況下系統就會把組件對象放入缺省的套間中(當然是運行在一個缺省的線程中).
套間的本質
套間是保存在線程的TLS中的一個數據結構,借用該結構使套間和線程之間建立起某種關係,通過該結構可以幫助不同的套間之間通過消息機制來實現函數的調用,以保證多線程環境下,數據的同步。

STA

Figure 1


一個STA只能包含一個線程,但是可以包含很多的COM對象。一個套間至少包含一個線程。特別的是,如果有對象要給其他線程用,那麼這個線程必須有消息循環。

STA的訪問規則
1.STA線程中創建的所有STA對象都會和線程在一個套間內
2.所有STA對象的調用都是通過這個線程來實現的

Figure 2

通過上面圖片表明,兩個不同套間的線程在同一個DLL中創建對象,也屬於不同的套間
對於第二點,有兩個方式去調用對象的方法:
1. 在相同STA的線程內調用,調用方法直接順序調用
2. 在另外一個套間內的線程調用,這個時候COM要保證通過套間線程內調用,那就必須有消息循環。

消息循環

Figure 3


一個包含消息循環的線程,大家都知道的就是UI線程, 一個包含一個或者多個窗口的UI線程,通常會表示說這個線程包含了這麼多的窗口,窗口的調用都是通過擁有這個窗口的線程來實現的,這就是這個消息循環中的API DispatchMessage().
一個線程可以通過post/send message到這個從窗口,然後目標窗口的線程就會GetMessage(),然後去執行DispatchMessage(),所以對這個窗口的任何調用都是同步的,順序執行的。線程的消息循環就保證了這種同步,這樣開發人員就不需要去處理同步的問題或者多線程調用的問題,因爲只
有一個消息處理完了,纔會處理下一個消息。

同樣COM內嵌的窗口也同樣可以實現線程安全,所有從套間外調用COM對象的方法都是通過COM發送消息到隱藏的窗口來完成的,隱藏的窗口就會把收到的消息進行排序一個一個處理,然後返回調用結果。

當涉及到套間外調用方法,COM就會準備proxies和stubs,從這點看消息循環只是STA的一個協議。

有兩點要注意:
1. 上面提到的通過消息循環處理STA COM對象的調用,只是當套間外調用的時候,如果是套間內調用,就不需要走COM
2. 如果STA中的消息循環失敗,不能get和dispatch消息,那麼套間線程就不會再收到其他套間發來的消息。
考慮到第2點,有些API,比如Sleep, WaitForSingleObject, WaitForMultipleObjects將會破壞消息循環的流程,

STA的好處:
用STA的第一個好處就是簡單,在COM類型內除了基本的代碼,很少或者根本不需要同步代碼,COM會自動同步,順序執行操作,這一點對ActiveX特別有用。
因爲STA對象由線程的親緣性(所有操作都是同一線程來執行的),那麼STA對象的開發人員,可以在這個線程內跟蹤對象的數據。

STA的壞處:
STA架構會造成performance的問題,如果有多個線程去訪問對象的方法,那麼由於STA只有一個消息循環,那麼就會多個線程同時等待,順序執行這些調用。同樣要是STA包含多個COM對象,也會導致

實現COM對象和服務

STA COM可以使開發人員忽略併發修改COM對象內部數據,但是全局數據的修改或者全局函數的調用STA COM就不會保護,比如全局函數DllGetClassObject等,主要是因爲STA對象可以在不同的線程內創建。創建的套間不同,可以同時訪問全局變量或者函數方法。
大家都知道的COM對象個數的全局變量,這個變量在DllGetClassObject和DllCanUnloadNow中修改,所以在這兩個函數中,利用了InterlockedIncrement和InterlockedDecrement來實現同步,
因此在實現COM服務的時候要注意:
1. DLL 服務器必須包含線程安全標準函數 DllGetClassObject()和DllCanUnloadNow(),
2. 私有的全局函數必須保證線程安全
3. 私有的全局變量必須保證線程安全

默認STA
int main()
{
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2;
    /* If a default STA is to be created and used, it will be created */
    /* right after spISimpleCOMObject2 (an STA object) is created. */
    spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

在這個main函數中,我們分析一下:
1. 在main函數中調用了::CoInitializeEx(NULL, COINIT_MULTITHREADED);, 表明該線程是MTA線程
2. 調用DisplayCurrentThreadId()去顯示當前線程的ID。
3. 創建STA COM對象spISimpleCOMObject2。
4. 注意到spISimpleCOMObject2是STA對象,而當前線程不是STA線程,這時候COM就會創建一個default STA。
5. 調用TestMethod1()(該函數打印調用的線程),就會發現線程的ID不是main線程的ID。
所有的STA COM對象如果在非STA裏創建都會屬於這個default STA 裏面,與創建這些STA對象的線程不同套間

Figure 4


通過圖顯示,spISimpleCOMObject2->TestMethod1()是跨套間的調用, 這需要marshal, 因此main創建的COM對象返回的實際上是proxy,不是真正的指針。

下面一個例子可以證明,非STA創建的STACOM對象實際上實在default STA的。
int main()
{
  HANDLE hThread = NULL;
  DWORD  dwThreadId = 0;
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2;
    spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2 -> TestMethod1();
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL, // SD
      (SIZE_T)0,      // initial stack size
      (LPTHREAD_START_ROUTINE)ThreadFunc, // thread function
      (LPVOID)NULL,                       // thread argument
      (DWORD)0,                    // creation option
      (LPDWORD)&dwThreadId         // thread identifier
    );
    WaitForSingleObject(hThread, INFINITE);
    spISimpleCOMObject2 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

DWORD WINAPI ThreadFunc(LPVOID lpvParameter)
{
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2A;
    ISimpleCOMObject2Ptr spISimpleCOMObject2B;
    spISimpleCOMObject2A.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2B.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2A -> TestMethod1();
    spISimpleCOMObject2B -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

具體分析一下:
1. 在main 函數中調用 CoInitializeEx(NULL, COINIT_MULTITHREADED) ,表明該線程屬於MTA
2. 調用DisplayCurrentThreadId顯示main線程的id, 比如thread_id_1
3. 創建STA COM對象 spISimpleCOMObject2
4. 調用TestMethod1,這時候顯示的調用線程爲thread_id_2,不是主線程。
5. 啓動一個新的線程執行函數ThreadFunc(),在函數裏面初始化該線程屬於MTA
6. ThreadFunc調用DisplayCurrentThreadId顯示該線程的id,假設爲thread_id_3.
7. 創建兩個STA對象spISimpleCOMObject2A和spISimpleCOMObject2B
8. 對兩個對象調用TestMethod1(), 發現輸出的ID是thread_id_2.
該例看出來spISimpleCOMObject2, spISimpleCOMObject2A和 spISimpleCOMObject2B都屬於default STA 

Legacy STA (ThreadingModel 是Single)
Legacy STA 與標準的STA有一點不同,就是所有的Legacy STA COM 對象都屬於一個Legacy STA, 一個線程中。
 

Figure 5

可以看出非STA線程創建的legacy STA對象獲得的實際上是被Marshal過的,創建線程返回的是Proxy的對象,不是實際的指針,這點跟標準STA一樣。


STA之間COM對象的互相調用
確切的說只要是套間之間的COM對象的互相調用,都要對COM對象進行列集(Marshal),如果不列集,散集,那麼就沒有利用COM的特性,實際上是直接調用的對象,就需要對數據進行保護,而如果進行了列集,那麼就COM 就會自動對接口的調用進行序列化,STA套間就會保證COM對象的順序調用,即使不同
的線程同時調用方法,套間也會保證COM對象的調用實際是在套間內線程調用的, COM對象所在的套間是STA套間,則就一個線程,如果是MTA,則必須對COM對象進行數據,防止多個MTA內線程同時調用。

 

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