在COM時代裡,套間是用來簡化多執行緒環境下使用COM物件的,然而在.NET裡面,微軟又放棄了套間的概念,這樣給我們在.NET裡面使用COM物件的時候造成了很多的麻煩。例如有的時候你會發現在有的執行緒裡面建立了COM物件並將它的引用儲存在全域性變數裡面,在其他的執行緒裡面使用的時候,卻發現.NET扔出一個InvalidCastException的異常,發生這種情況大多數都是因為兩個.NET執行緒執行在不同的套間引起的。比如下面的COM伺服器和C#客戶端:
C#客戶端的原始碼
1. using System; |
|
2. using System.Collections.Generic; |
|
3. using System.Linq; |
|
4. using System.Runtime.InteropServices; |
|
5. using System.Text; |
|
6. using System.Diagnostics; |
|
7. using System.Security.Cryptography; |
|
8. using System.Security.Principal; |
|
9. using Microsoft.Win32.SafeHandles; |
|
10. using System.ComponentModel; |
|
11. using System.Reflection; |
|
12. using System.Security; |
|
13. using System.IO; |
|
14. using System.Threading; |
|
15. using System.Security.Permissions; |
|
16. |
|
17. using ApartmentComponentLib; |
|
18. |
|
19. namespace CSharpQuestions |
|
20. { |
|
21. public class Watcher |
|
22. { |
|
23. private object m_IStaObject = null; |
|
24. |
|
25. [STAThread] |
|
26. public static void Main() |
|
27. { |
|
28. Console.WriteLine(Thread.CurrentThread.GetApartmentState()); |
|
29. Watcher watcher = new Watcher(); |
|
30. watcher.Initialize(); |
|
31. watcher.CreateThreads().Join(); |
|
32. |
|
33. Console.WriteLine(“Press any key”); |
|
34. Console.ReadLine(); |
|
35. } |
|
36. |
|
37. private Thread CreateThreads() |
|
38. { |
|
39. Thread thread = new Thread(ThreadFunc); |
|
40. thread.Start(); |
|
41. |
|
42. return thread; |
|
43. } |
|
44. |
|
45. private void ThreadFunc() |
|
46. { |
|
47. Console.WriteLine(Thread.CurrentThread.GetApartmentState()); |
|
48. IStaObject2 obj = (IStaObject2)m_IStaObject; |
|
49. obj.TestMethod(); |
|
50. } |
|
51. |
|
52. private void Initialize() |
|
53. { |
|
54. m_IStaObject = new StaObject2Class(); |
|
55. } |
|
56. } |
|
57. } |
COM伺服器端
IDL檔案
1. import “oaidl.idl”; |
2. import “ocidl.idl”; |
3. |
4. [ |
5. object, |
6. uuid(34CF395D-F7F8-41FC-9074-E966304DA425), |
7. dual, |
8. nonextensible, |
9. helpstring(“IStaObject Interface”), |
10. pointer_default(unique) |
11. ] |
12. interface IStaObject : IDispatch{ |
13. [id(1), helpstring(“method TestMethod”)] HRESULT TestMethod(void); |
14. }; |
15. [ |
16. object, |
17. uuid(2451960E-F141-4F09-AB71-124E62B6A25E), |
18. helpstring(“IStaObject2 Interface”), |
19. pointer_default(unique) |
20. ] |
21. interface IStaObject2 : IUnknown{ |
22. HRESULT TestMethod(void); |
23. }; |
24. [ |
25. uuid(E410D347-D200-4362-82B8-F3361FA54446), |
26. helpstring(“ApartmentComponentLib Type Library”) |
27. ] |
28. library ApartmentComponentLib |
29. { |
30. importlib(“stdole2.tlb”); |
31. [ |
32. uuid(2C0624C9-4C88-4114-A165-9E4AA59A241F), |
33. helpstring(“StaObject Class”) |
34. ] |
35. coclass StaObject |
36. { |
37. [default] interface IStaObject; |
38. }; |
39. [ |
40. uuid(274BDE35-680D-48DC-A2F3-3AE26E7700DA), |
41. helpstring(“StaObject2 Class”) |
42. ] |
43. coclass StaObject2 |
44. { |
45. [default] interface IStaObject2; |
46. }; |
47. }; |
標頭檔案
1. // StaObject2.h : Declaration of the CStaObject2 |
2. |
3. #pragma once |
4. #include “resource.h” // main symbols |
5. |
6. #include “ApartmentComponent_i.h” |
7. |
8. #if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA) |
9. #error “Single-threaded COM objects are not properly supported on Windows CE platform” |
10. #endif |
11. |
12. // CStaObject2 |
13. class ATL_NO_VTABLE CStaObject2 : |
14. public CComObjectRootEx<CComSingleThreadModel>, |
15. public CComCoClass<CStaObject2, &CLSID_StaObject2>, |
16. public IStaObject2 |
17. { |
18. public: |
19. CStaObject2() |
20. { |
21. } |
22. |
23. DECLARE_REGISTRY_RESOURCEID(IDR_STAOBJECT2) |
24. |
25. |
26. BEGIN_COM_MAP(CStaObject2) |
27. COM_INTERFACE_ENTRY(IStaObject2) |
28. END_COM_MAP() |
29. |
30. |
31. |
32. DECLARE_PROTECT_FINAL_CONSTRUCT() |
33. |
34. HRESULT FinalConstruct() |
35. { |
36. return S_OK; |
37. } |
38. |
39. void FinalRelease() |
40. { |
41. } |
42. |
43. public: |
44. STDMETHOD(TestMethod)(void); |
45. |
46. }; |
47. |
48. OBJECT_ENTRY_AUTO(__uuidof(StaObject2), CStaObject2) |
CPP檔案
1. #include “stdafx.h” |
2. #include “StaObject2.h” |
3. #include <iostream> |
4. |
5. using namespace std; |
6. |
7. // CStaObject |
8. STDMETHODIMP CStaObject2::TestMethod(void) |
9. { |
10. cout << “CStaObject2::TestMethod” << endl; |
11. |
12. return S_OK; |
13. } |
Rgs檔案
1. HKCR 2. { 3. ApartmentComponent.StaObject2.1 = s `StaObject2 Class` 4. { 5. CLSID = s `{274BDE35-680D-48DC-A2F3-3AE26E7700DA}` 6. } 7. ApartmentComponent.StaObject2 = s `StaObject2 Class` 8. { 9. CLSID = s `{274BDE35-680D-48DC-A2F3-3AE26E7700DA}` 10. CurVer = s `ApartmentComponent.StaObject2.1` 11. } 12. NoRemove CLSID 13. { 14. ForceRemove {274BDE35-680D-48DC-A2F3-3AE26E7700DA} = s `StaObject2 Class` 15. { 16. ProgID = s `ApartmentComponent.StaObject2.1` 17. VersionIndependentProgID = s `ApartmentComponent.StaObject2` 18. InprocServer32 = s `%MODULE%` 19. { 20. val ThreadingModel = s `Free` 21. } 22. `TypeLib` = s `{E410D347-D200-4362-82B8-F3361FA54446}` 23. } 24. } 25. } |
將COM 伺服器註冊,然後使用tlbimp.exe生成一個可以被C#客戶端程式引用的IA(Interop Assembly),並且編譯執行上面的C#客戶端程式,你會發現.NET會丟擲一個System.InvalidCastException異常:
System.InvalidCastException occurred Message=”Unable to cast COM object of type `ApartmentComponentLib.StaObject2Class` to interface type `ApartmentComponentLib.IStaObject2`. This operation failed because the QueryInterface call on the COM component for the interface with IID `{2451960E-F141-4F09-AB71-124E62B6A25E}` failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).” Source=”ApartmentComponentLib” StackTrace: at ApartmentComponentLib.StaObject2Class.TestMethod() InnerException: |
從高亮顯示的訊息裡面可以看出,當我們試圖在另外一個執行緒使用另一個執行緒建立的物件的時候,查詢所需要的COM介面失敗—也就是為什麼.NET扔出來一個InvalidCastException。
這就是一個典型的跨套間使用COM物件失敗的例子,因為跨套間使用COM物件時,COM要求所使用的COM介面是可列集(Marshal)的。如果所要求(QueryInterface)的介面不能被列集(Marshal),在呼叫端那裡QueryInterface返回E_NOINTERFACE,雖然看起來好像是不支援所查詢的介面,實際上是因為COM沒有辦法將介面從一個套間列集到另外一個套間裡面去。
COM裡面,套間是一個 想象中的邊界,用來在多執行緒環境中安全使用執行緒安全和執行緒不安全的COM物件。什麼叫做執行緒安全的COM物件呢?再多執行緒環境中,如果這個COM物件自己實現了同步機制,可以被多個執行緒同時呼叫而不破壞物件內部資料的完整性的話,那麼這個物件就叫做執行緒安全的物件。然而COM物件有一個目標就是,即使在多執行緒環境裡面也可以安全地使用執行緒不安全的COM物件。也就是說,即使COM物件內部沒有實現同步機制,COM也有一個機制可以建立一個執行緒安全的環境來使用這個物件,這個機制就是套間。在多執行緒環境裡面,套間為執行緒不安全的COM物件建立了一個同步機制,COM保證在任意時刻都只有一個客戶端在呼叫執行緒不安全的COM物件(呼叫它的函式—COM世界裡面只有函式和介面)。
關於套間的知識,可以參考下面兩篇文章:
http://www.codeguru.com/cpp/com-tech/activex/apts/article.php/c5529
http://www.codeguru.com/cpp/com-tech/activex/apts/article.php/c5533
而如果需要跨套間呼叫COM物件,這個函式呼叫,呼叫使用的引數和函式呼叫返回值都需要在套間之間被列集。而如果你的引數裡面使用到了COM介面的話,例如跨套間使用一個COM物件,並且呼叫這個物件的QueryInterface方法,QueryInterface返回的介面就需要被列集。COM庫使用CoMarshalInterThreadInterfaceInStream 和CoGetInterfaceAndReleaseStream來列集介面。
CoMarshalInterThreadInterfaceInStream 查詢登錄檔HKEY_CLASSES_ROOT”Interface”{IID}”ProxyStubClsid32中要列集的介面是否註冊有列集程式(Proxy和Stub程式)。
1. 如果這個鍵值存在,CoMarshalInterThreadInterfaceInStream會啟用裡面CLSID對應的COM物件來完成介面的列集;
2. 如果沒有這個鍵值,那麼說明沒有提供方法列集介面,因此QueryInterface返回E_NOINTERFACE。
如果你細心一點的話,會發現很多介面的ProxyStubClsid32裡面的CLSID是一樣的,而且這些介面通常都會有另外一個子鍵:TypeLib。這是因為手工編寫處理介面列集的COM物件的工作繁瑣又容易出錯,
1. 所以對於一些Dual介面,COM庫(實際上是OLEAUT32.dll)提供了一個通用的類來列集所有的Dual介面,它所需要的就是型別庫檔案—因為型別庫裡面包含了所有COM物件的後設資料(Meta Data);
2. 另外,對於非Dual介面,你也可以使用MIDL根據IDL檔案生成對應的列集介面的COM物件。
這是在上一篇文章裡面例子程式裡面出現InvalidCastException的原因。
由於所有的COM物件都會被分配到一個相應的套間裡面,因此在.NET裡面,為了方便.NET程式呼叫COM物件,每一個.NET執行緒都會被分配到一個套間裡面――即使你沒有在程式碼裡面指定執行緒執行的套間。在.NET執行緒裡面建立的COM物件都會被分配到特定的套間裡面,如果兩個.NET執行緒 被分配到了不同的套間裡,那麼兩個執行緒之間互相呼叫COM物件就需要列集函式呼叫。
在.NET 2.0以後,預設情況下.NET的執行緒是執行在多套間(MTA)裡面的,但是在Visual Studio裡面建立C#工程的時候,Visual Studio的專案模板會在你程式碼的Main函式上加上[STAThread]屬性,表示主執行緒執行在STA套間裡面,這樣就會造成.NET程式有執行緒執行在兩個不同的套間裡面:
[STAThread] public static void Main() |
在Main函式上面加上[STAThread]屬性會使.NET將主執行緒放在STA套間裡面,而加上[MTAThread]屬性則會將主執行緒放在MTA套間裡面。
而對於其他執行緒,則需要使用Thread.SetApartmentState()函式來設定執行緒所執行的套間,而這個函式必須線上程啟動之前呼叫,也就是在Thread.Start ()之前呼叫,這也是為什麼.NET提供一個[STAThread]屬性和[MTAThread]屬性的原因――因為你沒有辦法在主執行緒啟動之前設定主執行緒執行的套間。線上程裡面,你可以使用Thread.GetApartmentState()函式來獲取執行緒所執行的套間資訊。
在COM套間對.NET程式使用COM物件的影響(上)文章裡面的程式碼的修復方案如下,即將主執行緒的STAThread屬性去掉,讓大家都執行在程式裡面唯一一個MTA套間裡面,這樣就沒有列集介面的問題了:
1. using System; 2. using System.Collections.Generic; 3. using System.Linq; 4. using System.Runtime.InteropServices; 5. using System.Text; 6. using System.Diagnostics; 7. using System.Security.Cryptography; 8. using System.Security.Principal; 9. using Microsoft.Win32.SafeHandles; 10. using System.ComponentModel; 11. using System.Reflection; 12. using System.Security; 13. using System.IO; 14. using System.Threading; 15. using System.Security.Permissions; 16. 17. using ApartmentComponentLib; 18. 19. namespace CSharpQuestions 20. { 21. public class Watcher 22. { 23. private object m_IStaObject = null; 24. 25. public static void Main() 26. { 27. Console.WriteLine(Thread.CurrentThread.GetApartmentState()); 28. Watcher watcher = new Watcher(); 29. watcher.Initialize(); 30. watcher.CreateThreads().Join(); 31. 32. Console.WriteLine(“Press any key”); 33. Console.ReadLine(); 34. } 35. 36. private Thread CreateThreads() 37. { 38. Thread thread = new Thread(ThreadFunc); 39. thread.Start(); 40. 41. return thread; 42. } 43. 44. private void ThreadFunc() 45. { 46. Console.WriteLine(Thread.CurrentThread.GetApartmentState()); 47. IStaObject2 obj = (IStaObject2)m_IStaObject; 48. obj.TestMethod(); 49. } 50. 51. private void Initialize() 52. { 53. m_IStaObject = new StaObject2Class(); 54. } 55. } 56. } |