【转】STA 和 MTA
Overview
COM技术过时了吗?这句话也对也不对。从技术上讲,确实COM的使用率在下降,但是从思想上来说,COM的面向接口的思想正在被Java和.NET发扬光大。那我们还需要和COM打交道吗?这取决于你工作的领域。虽然现在微软的平台在慢慢向着.NET迁移,不过,在维护原有非托管代码,编写和Windows系统组件打交道的程序,以及使用CLR调用非托管代码的时候,COM或多或少都是不可避免的。与COM打交道就没法不谈到套间(Apartments)。套间是COM中一个非常有用然而也非常难以理解的一个概念,可以说COM中的很多问题都和套间有关,理解了套间,离完全理解COM就更近了一步,本文将分若干次讨论套间的基础知识以及在.NET中的应用。
什么是套间(Apartments)
套间是COM为了简化对象对多线程的支持而推出的一套机制,用于指定线程和COM对象的多线程特性,并且对不同特性的套间之间的调用提供同步支持,保证不同多线程特性的对象之间可以互相正确调用而不会引入同步问题,简化编程(实际上可能搞得更复杂了)。比如,如果某个对象编写的时候忘记考虑多线程,或者没有时间考虑,或者没有必要提供实现多线程的支持,这个时候可以将对象指定为STA,让COM自动管理对该对象的调用,保证对象可以被正确调用,即使是多线程的调用也会被串行化(依次调用,而非同时调用)。反之,如果一个对象支持多线程调用,那么它可以被标记为MTA,COM会允许对其进行多线程的调用。
对于套间需要注意下面几点:
1、套间并不是一个真实存在的一个区域,而是一个逻辑的概念
2、套间表明了位于套间的代码的多线程特性,决定了以下几点
a. 代码本身允许单线程调用还是多线程调用
b. 代码创建COM对象拿到的是Proxy还是原始的指针(关于Proxy请参见后面的Proxy一节)
c. 代码调用同一套间的COM对象是通过原始指针,不同套间则通过Proxy
3. 线程必须属于某个套间,这表明了线程本身的多线程特性,也就是线程会对COM对象进行多线程的调用还是单线程的调用。比如,一个线程位于STA中,那么该线程只适合直接调用支持单线程调用的、同一套间的COM对象,其他COM对象则需要通过Proxy来间接调用,什么是Proxy后面会讲到。同时,线程所处的套间还决定了创建对象的时候所获得的对象是对象本身还是Proxy。线程在同一时间内只能属于一个套间或者不属于某个套间,但是线程可以在不同时间内属于不同的套间。典型的例子有,线程调用了CoInitialize,然后再调用CoUninitialize退出套间,之后又调用了CoInitialize进入了另外一个套间,此外,线程临时进入NTA也是一个例子。下面会讲到。
4. COM对象也必须属于某个套间,同样的这决定了COM对象的多线程特性,和上面类似。COM对象不会从一个套间迁移到另外一个套间,如果这个套间被释放,那么这个对象也同时被释放,这个事实对于STA套间尤为重要。
5. 跨套间必须要通过Proxy,这是COM保证套间能够工作的基础。后面Proxy一节会谈到为什么是这样
套间(Apartments)的类型
常见的套间有STA和MTA,此外Win2000中引入了一种新的套间NTA。STA用于单线程,MTA用于多线程。而NTA则被称为线程无关(Thread-Neutral)的多线程。简单来讲,STA,MTA,NTA的区别请见下表:
线程和套间
线程通过调用CoInitialize/CoInitializeEx进入套间,然后通过CoUninitialize退出套间。进入套间可能会导致套间被创建,同样CoUninitialize调用会导致套间被释放。CoInitialize和CoUninitialize的调用次数必须Match,类似AddRef/Release。
CoInitialize只能进入STA套间,而CoInitializeEx可以通过传入参数进入不同的套间,传入COINIT_APARTMENTHREADED进入STA,而传入COINIT_MULTITHREADED则进入MTA。当调用了CoInitialize/CoUnintiialize之后,线程便属于了这个套间,如果指定STA,那么新的STA总会被创建,如果指定的是MTA,那么如果MTA不存在的话将创建一个新的MTA。细心的朋友可能已经注意到了,上面提到了3种套间,那么NTA跑哪去了呢?其实一个线程并不能属于NTA,线程只可以临时进入NTA,NTA中只可以存在对象。
对象和套间
COM对象总是属于某个套间的。COM对象在注册表里面可以通过ThreadingModel属性指定对象所期望的套间类型,有效的值有:
需要说明的是,从套间角度来讲主STA和其他非主STA没有区别,只是特别指定是主STA而已。
线程套间和对象套间的关系
大家可以看到,线程也有套间,同时对象也有套间,那么这两者有何关系呢?这是一个比较Confusing的一个问题。事实上,简单来讲,对象的套间设置决定了对象所处的套间,而线程的套间决定了线程的套间。OK,看到这里你可能会说,这不是等于没说吗?呵呵,这确实是最本质的区别,然而,另外这两个套间的设置还决定了另外一点,即套间和对象是否兼容,是否处于同一套间。这很重要,因为这决定的了CoCreateInstance所返回的对象的指针是原始指针还是Proxy(这里讨论进程内的情况,进程外则总是Proxy)。举例来讲,如果线程的套间是STA,并且对象的套间也是STA,那么这个对象就被创建在线程所位于的STA中,反之,如果线程的套间是STA,而对象的套间是MTA,那么对象则被创建到唯一的MTA套间中,线程拿到的是对象的Proxy(代理),而非原始指针。代理的概念后面会讲到。
MSDN中有一张表,这里稍作修改,列在下面:
跨套间(Cross-Apartment),Proxy/Stub以及Marshalling
套间调用本套间内的对象不需要Proxy,则是直接调用,和普通C++的虚函数调用并无区别。COM强大的地方(也是不太容易理解的地方)在于可以通过Proxy来实现线程安全。我们还是用一个实际的例子来考虑这个问题,假如两个MTA线程同时调用一个STA中的对象A,这个对象因为位于STA中,因此它编写的时候没有考虑到多线程问题,因此需要保护。如果两个MTA线程同时通过A的指针pA来调用A的方法,显然这个时候是无法提供线程安全的保护的。COM的解决方案是,让这两个MTA线程拿到的对象A并非对象A本身,而是A的Proxy。所谓Proxy,指的是该对象并非是实际对象,而是一个代理,负责将调用转发到它所代理的对象A,代理本身并不执行实际操作。而在服务器端,有一段代码称之为Stub,负责接受Proxy发来的请求,并实际执行这个请求。换句话说,Proxy总是在客户端,而Stub则是在服务器端。