.NET中STAThread和MTAThread


本文討論在.NET中使用進程內COM組件時的公寓模型,以一個示例直觀演示STAThread和MTAThread的作用和區別。

1. COM中的公寓

1.1 基本規則

公寓是COM組件的運行環境,日常生活中公寓是用來住人的,COM中的公寓是用來住COM組件的對象的,每個COM對象必須且只能位於一個公寓中:單線程公寓(STA)或多線程公寓(MTA)。

每個進程可以有0或多個STA。

每個進程可以有0或1個MTA。

一個線程只能關聯到一個公寓。因此所有關聯到MTA的線程都是關聯到進程唯一的一個MTA。

本線程訪問與本線程關聯的STA中的COM對象不需要列集,直接訪問。

其他線程對STA中的COM對象的訪問需要列集(marshal),通過列集,自動實現了多線程訪問下的同步

所有線程對MTA中的COM對象的訪問不需要列集,直接訪問,需要COM組件自身實現多線程下的同步

(列集就是將函數調用序列化,實現跨邊界調用,在Windows中通常是通過消息機制實現。在COM中RPC就是列集,在WinForm中Control.Invoke就是一種列集,Remoting也是列集,WCF也是列集,最近流行的RESTfull也是。。。)

1.2 公寓類型匹配

一個COM對象所屬的公寓,由兩個地方的配置確定:組件公寓模型客戶端線程公寓模型

  1. 組件公寓模型是在組件注冊到注冊表時設定,通過組件公寓模型,組件聲明自己可以住在什么樣的公寓里。可選項包括:Apartment,Free和Both。Apartment,我只能住在單線程公寓中;Free,我只能住在多線程公寓中;Both,我隨意,單線程公寓或多線程公寓都可以。
  2. 客戶端線程公寓模型就是線程的公寓模型,表示當前線程提供什么樣的公寓。可選項包括:單線程公寓(STA)或多線程公寓(MTA),也就是本文所討論的STAThread和MTAThread。

下表列出了組件對象最終會住在什么公寓中的組合表:

客戶端線程公寓模型 \ 組件公寓模型  Apartment Free Both
STA STA MTA STA
MTA STA MTA MTA

 

 

 

 

如果組件公寓模型為Apartment,不管客戶端線程公寓模型是什么,組件最后都住在STA中,因為組件說了“我只能住在單線程公寓中”。如果當前線程是MTA,COM庫會后台創建一個STA來放該組件的對象。

如果組件公寓模型為Free,不管客戶端線程公寓模型是什么,組件最后都住在MTA中,因為組件說了“我只能住在多線程公寓中”。如果當前線程是STA,COM庫會檢查當前進程的MTA有沒有創建,沒有就創建進程的MTA,然后將組件的對象放在MTA中。

如果組件公寓模型為Both,組件最后都住在與當前線程關聯的公寓中,如果當前線程是STA,它就住在STA中;當前線程是MTA,它就住在MTA中。本文中,我們會創建一個並注冊一個Both類型的組件,然后分別在STA和MTA中創建該組件的對象。

1.3 .NET中設置客戶端線程公寓模型 

在.NET中使用COM組件時,需要設置線程的公寓模型。 

在.NET中可以通過STAThread和MTAThread屬性來設置主線程的公寓類型, 通過Thread.SetApartmentState可以設置工作線程的公寓類型。

對於WinForm或WPF應用程序,主線程的公寓模型必須為STA,因為用戶界面對象都不是線程安全的。

對於控制台應用程序,主線程的公寓模型可以隨意設置,為了方便,我們用控制台應用程序來演示。(用WinForm也完全可以演示,只是需要在工作線程中創建組件的對象。)

2. 一個簡單的COM組件

為了演示單線程公寓和多線程公寓的區別,我們用ATL實現定義一個簡單的COM組件SimpleCom,該組件包含一個返回字符串的方法Hello,返回的字符串分三步合成,每步之間通過Consume方法來消耗較長CPU周期,確保Hello不會在操作系統的一個時間片內被執行完成,保證Hello函數被並發執行,以達到演示的效果。代碼如下:

 1 // CSimpleCom.h
 2 class ATL_NO_VTABLE CSimpleCom :
 3     public CComObjectRootEx<CComSingleThreadModel>,
 4     public CComCoClass<CSimpleCom, &CLSID_SimpleCom>,
 5     public IConnectionPointContainerImpl<CSimpleCom>,
 6     public CProxy_ISimpleComEvents<CSimpleCom>,
 7     public IDispatchImpl<ISimpleCom, &IID_ISimpleCom, &LIBID_ATLTestLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
 8 {
 9 public:
10     CSimpleCom()
11     {
12         this->m_iMember = 0;
13     }
14 
15 DECLARE_REGISTRY_RESOURCEID(IDR_SIMPLECOM)
16 
17 
18 BEGIN_COM_MAP(CSimpleCom)
19     COM_INTERFACE_ENTRY(ISimpleCom)
20     COM_INTERFACE_ENTRY(IDispatch)
21     COM_INTERFACE_ENTRY(IConnectionPointContainer)
22 END_COM_MAP()
23 
24 BEGIN_CONNECTION_POINT_MAP(CSimpleCom)
25     CONNECTION_POINT_ENTRY(__uuidof(_ISimpleComEvents))
26 END_CONNECTION_POINT_MAP()
27 
28 
29     DECLARE_PROTECT_FINAL_CONSTRUCT()
30 
31     HRESULT FinalConstruct()
32     {
33         return S_OK;
34     }
35 
36     void FinalRelease()
37     {
38     }
39 
40 public:
41     STDMETHOD(Hello)(BSTR* a);
42 private:
43     int m_iMember;
44     CString m_str;
45 };
46 
47 OBJECT_ENTRY_AUTO(__uuidof(SimpleCom), CSimpleCom)
 1 // CSimpleCom.cpp
 2 double Cosume()
 3 {
 4     double d = 0;
 5     for (int i = 0; i < 1000*1000*300; i++)
 6     {
 7         d += i;
 8     }
 9     return d;
10 }
11 
12 STDMETHODIMP CSimpleCom::Hello(BSTR* a)
13 {
14     m_str = L"0>你好! ";
15     Cosume();
16     CString str;
17     str.Format(L"1>m_iMember = %d; " , this->m_iMember++);
18     m_str += str;
19     Cosume();
20     m_str += L"2>再見~";
21     *a = m_str.AllocSysString();
22     return S_OK;
23 }

將組件的ThreadingModel設置為Both,生成項目,組件會自動注冊。

接下來創建C#客戶端,使用該組件。

3. C#客戶端

新建一個C#控制台應用程序,添加對SimpleCom組件的引用,在主線程中創建SimpleCom組件的對象,在兩個工作線程中同時調用該對象。

通過修改主線程的公寓類型,演示進程內COM組件對象在不同類型的公寓中的行為差異。

3.1 多線程公寓

在多線程公寓中創建SimpleCom組件的對象的代碼如下:

 1 namespace ConsoleApplication1
 2 {
 3     class Program
 4     {
 5         [MTAThread()]
 6         static void Main(string[] args)
 7         {
 8             var v = new ATLTestLib.SimpleCom();
 9             Thread t = new Thread(x =>
10             {
11                 Run((ATLTestLib.ISimpleCom)x);
12             });
13             t.SetApartmentState(ApartmentState.STA);
14             t.Start(v);
15             Thread.Sleep(300);
16             Thread t2 = new Thread(x =>
17             {
18                 Run((ATLTestLib.ISimpleCom)x);
19             });
20             t2.SetApartmentState(ApartmentState.STA);
21             t2.Start(v);
22         }
23 
24         static public void Run(ATLTestLib.ISimpleCom sc)
25         {
26             try
27             {
28                 for (var i = 0; i < 5; i++)
29                 {
30                     Console.WriteLine(string.Format("[{0}] {1}",
31                         Thread.CurrentThread.ManagedThreadId,
32                         sc.Hello()));
33                 }
34             }
35             catch (Exception ex)
36             {
37                 Console.WriteLine(ex);
38             }
39         }
40     }
41 }

運行結果:

[3] 0>你好! 1>m_iMember = 0; 1>m_iMember = 1; 2>再見~
[5] 0>你好! 2>再見~
[3] 0>你好! 1>m_iMember = 2; 1>m_iMember = 3; 2>再見~
[5] 0>你好! 2>再見~
[3] 0>你好! 1>m_iMember = 4; 1>m_iMember = 5; 2>再見~
[5] 0>你好! 2>再見~
[3] 0>你好! 1>m_iMember = 6; 1>m_iMember = 7; 2>再見~
[5] 0>你好! 2>再見~
[3] 0>你好! 1>m_iMember = 8; 1>m_iMember = 9; 2>再見~
[5] 0>你好! 1>m_iMember = 8; 1>m_iMember = 9; 2>再見~2>再見~
請按任意鍵繼續. . .

原理說明:

由於兩個線程的代碼能夠同時調用組件對象v的方法,組件中m_str的值被兩個線程同時修改,Hello方法返回的值出現了混亂,典型的缺乏的同步的結果。

3.2 單線程公寓

單線程公寓只需要將上面代碼中的MTAThread改為STAThread即可。

運行結果:

[3] 0>你好! 1>m_iMember = 0; 2>再見~
[4] 0>你好! 1>m_iMember = 1; 2>再見~
[3] 0>你好! 1>m_iMember = 2; 2>再見~
[4] 0>你好! 1>m_iMember = 3; 2>再見~
[3] 0>你好! 1>m_iMember = 4; 2>再見~
[4] 0>你好! 1>m_iMember = 5; 2>再見~
[3] 0>你好! 1>m_iMember = 6; 2>再見~
[4] 0>你好! 1>m_iMember = 7; 2>再見~
[3] 0>你好! 1>m_iMember = 8; 2>再見~
[4] 0>你好! 1>m_iMember = 9; 2>再見~
請按任意鍵繼續. . .

原理說明:

由於對STA中對象的調用都被COM運行時列集,自動對多線程調用實現了同步。 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM