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
的区别请见下表:
|
跨套间可以在任意线程上执行
|
线程可以属于该套间
|
多线程特性?
|
需要消息循环?
|
进程中套间个数
|
套间中线程个数
|
STA
|
No
|
Yes
|
单线程
|
Yes
|
无限制
|
1
|
MTA
|
No
|
Yes
|
多线程
|
No
|
1
|
无限制
|
NTA
|
Yes
|
No
|
多线程
|
No
|
1
|
无限制
|
在文章后面将详细解释各个套间的特点、区别以及编程的有关注意事项。
线程和套间
线程通过调用
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
属性指定对象所期望的套间类型,有效的值有:
属性值
|
含义
|
Main (缺省值)
|
主STA,也就是第一个创建的STA
|
Apartment
|
STA
|
Both
|
STA或者MTA都可以
|
Free
|
MTA
|
Neutral
|
NTA
|
需要说明的是,从套间角度来讲主STA
和其他非主STA
没有区别,只是特别指定是主STA
而已。
线程套间和对象套间的关系
大家可以看到,线程也有套间,同时对象也有套间
,那么这两者有何关系呢?这是一个比较Confusing
的一个问题。事实上,简单来讲,对象的套间设置决定了对象所处的套间,而线程的套间决定了线程的套间。OK
,看到这里你可能会说,这不是等于没说吗?呵呵,这确实是最本质的区别,然而,另外这两个套间的设置还决定了另外一点,即套间和对象是否兼容,是否处于同一套间。这很重要,因为这决定的了CoCreateInstance
所返回的对象的指针是原始指针还是
Proxy
(这里讨论进程内的情况,进程外则总是Proxy
)。举例来讲,如果线程的套间是STA
,并且对象的套间也是STA
,那么这个对象就被创建在线程所位于的STA
中,反之,如果线程的套间是STA
,而对象的套间是MTA
,那么对象则被创建到唯一的MTA
套间中,线程拿到的是对象的Proxy
(代理),而非原始指针。代理的概念后面会讲到。
MSDN
中有一张表,这里稍作修改,列在下面:
|
对象套间=Main
|
对象套间=Apartment
|
对象套间=Both
|
对象套间=Free
|
对象套间=Neutral
|
线程套间=主STA
|
主STA
|
当前线程套间
|
当前线程套间
|
MTA
|
NTA
|
线程套间=STA
|
当前线程套间
|
当前线程套间
|
当前线程套间
|
MTA
|
NTA
|
线程套间=MTA
|
主STA
|
STA
|
当前线程套间
|
MTA (当前线程的套间)
|
NTA
|
线程套间=NTA
|
主STA
|
STA
|
NTA
|
MTA
|
NTA
|
跨套间(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
则是在服务器端。


1. CoCreateInstance/CoCreateInstancEx
CoCreateInstance/CoCreateInstancEx
是创建COM
对象必须调用的函数,这个函数会根据上面一节所提到的表决定对象在那个套间中创建,如果对象在当前线程套间中创建,说明线程的套间和对象的套间是兼容的,不需要COM
则外处理,因此直接返回对象的原始的接口指针。反之,说明COM
对象和线程的套间不兼容,必须返回一个对象的接口的Proxy
来处理这些事情。
2. Marshalling/UnMarshalling
Marshalling/UnMarshalling
又称为列集和散集(直译),大致可以理解为Serialization/Deserialization
,也就是串行化和反串行化。套间之间互相传递接口指针必须要通过这个过程,其实在上面的那种情况就是一种常见的Marshalling/Unmarshalling
的特例。如果套间A
要把接口指针传到套间B
,不能直接把指针传过去,而一定要经过下面的过程:
a.
套间A
将接口指针Marshal
到一个IStream
对象(也是COM
对象),一般通过CoMarshalInterThreadInterfaceInStream
,更强大的函数则是CoMarshalInterface
。这个Marshal
是通过IMarshal
接口来实现的,COM
有缺省的IMarshal
实现。
b.
套间A
将IStream
对象的IStream
接口指针传递给套间B
c.
套间B
将IStream
接口指针指向的Stream
对象进行Unmarshal
,获得COM
对象基于本套间的Proxy
,一般通过CoGetInterfaceAndReleaseStream
,或者用UnmarshalInterface
,
细心一些的朋友可能会问到,且慢,你不是说接口指针需要Proxy
吗,那么IStream
本身不是也需要Proxy
吗?事实上,这个IStream
对象的接口指针是很特别的,它可以保证在不同套间中可以调用。
如果不遵循这个原则,直接传递指针会有什么结果呢:
1.
运气好的话,如果这个指针是一个Proxy
,那么可能会报告错误RPC_E_WRONGTHREAD
,表明Proxy
对应的套间并非本套间
2.
否则,在进行调用的时候,COM
的多线程保护将失去作用,你将会遇到各种多线程相关的问题
因此,一定要确保在不同套间之间传递接口指针的时候要Marshal/Unmarshal
指针来获得Proxy
。
OK,这一次就讨论到这里,下篇文章将着重讨论STA/MTA/NTA这几类套间。敬请关注。