前段时间碰到一个奇怪的com问题,具体描述当在主线程创建一个com对象时候,dynamic_cast能正确的转化com指针
但是当在线程中创建了这个com对象的时候dynamic_cast却会失败。
线程开始时也已经调用了CoInitilize,最奇怪的是创建其他com对象都可以,唯独新添加的com对象不行,当时这个问题也是废了1个晚上
没搞定,后来经过对比代码,发现在com的rgs文件里ThreadingModel里出错了,经过改正,一切OK。今晚想起这个问题就深究一下,从网上找了一篇相关解释,转帖过来,以后慢慢研究吧。
【Apartent的基本知识】
Apartment就是COM中并发访问的边界,是COM的基本线程安全单位。Apartment不
能同线程或者进程等Win32对象相比较,只是用于说明COM对象的线程规则:这些规则
因为Apartment的类型的不同而不同。它的作用就是提供对于非线程安全的组件提供
保护、序列化对于这些组件的访问,我们可以把Apartment想像成一个盒子,里面装
着组件和线程,Apartment可以同步对于对于里面的非线程安全组件的并发访问,如
果你申明了组件是线程安全的,COM这不会对访问这些对象的线程进行任何同步。
每一个要使用COM的线程和每一个这些线程所创建的组件都必须属于一个
Apartment,Apartment也从来不跨越进程边界。所以如果一个组件和它的客户端处于
不同的进程中,那么它们肯定属于不同的Apartment。当一个客户程序创建Inproc组件
时,COM要决定是把组件放在创建线程所属的Apartment中还是放到其他的Apartment
中,如果COM把组件放到了创建线程所属的Apartment中,那么线程可以直接访问对象,
而当COM把组件放到别的Apartment中时,对于组件的访问就要经过调度(Marshaling)
。
这就意味着即使使用的Inproc组件,如果客户和组件在不同的Apartment中,也要提供
Proxy/Stub对供调度使用,跨Apartment边界的调用和跨进程、跨机器的远程调用使用
一样的Proxy/Stub对。
目前有三种Apartment,它们分别是STA(Single-Threaded Apartment),MTA(Multi
-Threaded Apartment),NTA(Neutral-Threaded Apartment),其中NTA只在Win2k中得到
支持。
STA中只能属于一个线程,但是对于里面包含的对象的个数是没有限制的,COM也没
有限制进程中创建的STA的个数,特别地进程创建的第一个STA称为主STA。STA主要的特
性是对于STA中的对象的访问首先交给此STA中的线程处理,由于对STA中的对象的所有
访问都在同一个线程上执行,COM就串行化了对非线程安全性的组件的访问。
COM在进程第一次调用CoInitilize(NULL)或者对带有COINIT_APARTMENTTHREADED标
记的CoInitializeEx函数时会调用RegisterClass来注册OleMainThreadWndClass窗口
类,在这期间或者以后对CoInitilize(NULL)或者带有COINIT_APARTMENTTHREADED标记的
CoInitializeEx函数调用都将使COM自动地调用CreateWindowEx来为每个STA创建一个具
有OleMainThreadWndClass窗口类的隐藏窗口,这个隐藏窗口的消息队列被用来串行化并
分派COM方法调用。当COM的RPC通道中出现对STA的方法调用时,COM要给这个STA的隐藏
窗口发送表示这个方法调用的消息(message),STA中的线程然后取得此消息,它把这个
消息分派到隐藏窗口,隐藏窗口的窗口过程或者把这个调用交给Stub处理,或者直接在
组件上执行,因为每次只是取得、分派、处理一个消息,对于STA里的调用就进行了同步
。
MTA中没有线程个数的限制,任何线程都可以通过声明CoInitializeEx(NULL,COINIT
_MULTITHREADED)来进入MTA,另外一点和STA明显不同的是一个进程只能有一个MTA。MTA
没有隐藏的窗口也没有消息队列来对调用进行同步,对于MTA中组件的调用出现在PRC线
程池中,COM随机地从中选择一个然后交给组件处理。因为没有进行同步,所以MTA中的
组件必须是线程安全的。
Win2k中引入了另外一种Apartment类型,这就是NTA,每一个进程只能有一个NTA,
NTA中不容纳线程,它只是对象的容器,线程不能被赋予到NTA中。NTA的一个特点是对于
NTA里面的对象的调用不会引起线程切换,换句话说就是当STA或者MTA中的线程要访问
NTA中的对象是,线程暂时地离开STA或者MTA,然后在NTA上执行调用。而STA和MTA上的
对象调用都是由Apartment本身里面的线程来执行的,所以跨Apartment的调用必须有线
程切换。因为没有线程切换,所以NTA可以认为是对跨Apartment的调用的一种优化。
Win2k还提供了一些特别高效的机制来同步调用,这里就不多说了。
【线程是如何被赋予到Apartment的:】
我们知道在我们的程序中如果要用到COM,就必须调用CoInitilize[Ex],
这两个函数的目的就是给线程初始化COM,并且把线程赋予Apartment。而到底赋
予到哪个Apartment取决于调用的函数和传递的参数。
1、如果线程调用的是CoInitilize,COM就构建一个新的STA,并且把这
个线程赋予新的STA。
2、如果调用的CoInitializeEx (NULL, COINIT_APARTMENTTHREADED),
COM做的工作和第一步是一样的。
3、如果线程调用CoInitializeEx (NULL, COINIT_MULTITHREADED),COM
会检查进程中是否存在一个MTA,如果存在就直接把线程赋予MTA,如果还没有MTA
存在,则首先创建MTA,然后在把线程赋予该MTA。
当COM创建一个Apartment的时候,首先在Heap上创建Apartment对象,然
后初始化一些必要的信息(例如:Apartment的ID和类型等等),当COM把线程赋予
Apartment时,在线程的TLS上记录线程对象的地址,这样COM库在运行时如果想要
知道线程属于那个Apartment,就到线程的TLS上读取Apartment的地址就行了。
Inproc组件是如何被赋予到Apartment中的
COM读取注册表中组建的ThreadingModel值来决定把在什么Apartment中创
建对象,ThreadingModel在InprocServer32分支下,有五种可能的取值,分别是
None、Apartment、Free、Both和Neutral(Windows2000才有)。可以用来创建这
几种的Apartment类型的对应关系如下所示:
None <--> 主STA
Apartment <--> 任何STA
Free <--> MTA
Both <--> MTA 或者 STA
Neutral <--> NTA(只存在于Win2K中)
当线程创建对象的时候,COM总是尽可能地将对象创建到与线程相同的Apartment
中,比喻说一个STA中的线程创建ThreadingModel=Apartment的对象,这时候对象
将创建在线程所属的STA中;如果MTA中的线程创建ThreadingModel=Free的对象,
对象将创建在线程所属的MTA中;然而如果STA中的线程创建ThreadingModel=Free
的对象时,对象将创建在进程的MTA中;MTA中的线程创建ThreadingModel=None或
者ThreadingModel=Apartment的对象时,对象将创建到STA中。
为什么ThreadingModel=None的对象总是在主STA中创建呢?ThreadingModel
= None的对象属于历史遗留问题,产生于COM还不支持多线程的时候,COM为了保
证这些对象的线程安全性,就把这些对象都创建在主STA中。试想一下假如有两个
readingModel=None的对象在同一个dll文件中,而这些对象要访问dll中的全局数
据,COM把对象都创建在主STA中,不是可以防止对象同时读写dll中的全局数据吗?
如果写程序的时候声明对象的线程模式为Free或者Both,那么程序员必须保证对
象的线程安全性,因为这时候COM不会作任何动作来保证线程安全。
【Outproc组件是如何被赋予到Apartment中的】
进出外组件没有TheadingModel属性,这是因为COM采用了一种完全不同的
方法来把组件赋予到Apartment,一般来说COM把组件创建在服务器进程中的创建
组件的线程所属的Apartment中。大多数Exe COM服务器在它们的主线程中调用
CoInitilize[Ex]以使主线程属于主STA,然后主线程调用CoRegisterClassObject
来注册类工厂,当服务器接受到激活请求时,创建组件的请求在主STA得到处理,
因而对象也是在主STA中创建的。
如果希望在MTA中创建对象,那么就必修把调用相应CoRegisterClassObject
的线程赋予到MTA中,这样激活请求在MTA中得到处理,在MTA中创建组件。
综上所述:对于Exe COM服务器,调用CoRegisterClassObject的线程所属的
Apartment中的类工厂创建的组件属于这个Apartment。这其中也有少量例外的情
况:用ATL些的组件如果CComAutoThreadModule和CComClassFactoryAutoThread有
所不同,关于ATL的具体介绍,过几天我想专门写几篇文章说说,这里就不在写了。
【总结】
1、每个STA只能有一个线程,但是每个进程的STA数量是没有限制的。对于STA
中方法调用,总是被送到STA中唯一的线程来处理,所以STA一次只能接收和处理
一个方法调用。
2、MTA中可以有任意多的线程,但是每个进程只能有一个MTA。对于MTA中的方
法调用将不被同步地分派,所以MTA中的对象和线程必须是线程安全的。
3、NTA中没有线程,而且每个进程只能有一NTA。对于NTA中的方法调用不会引
起线程切换,调用的线程暂时离开自己的Apartment,在NTA上直接执行调用。
下面我们在来说说如何正确的写代码,这包括客户端和服务器端两个方面。
【客户端】
1、客户端线程必须调用CoInitialize[Ex]
任何需要COM有关服务的线程都必须声明CoInitialize[Ex]来初始化COM,在
CoInitialize[Ex]这个API函数中,线程被赋予到一个Apartment中,当然在线程结束
的时候也不要忘记调用CoUninitialize来释放CoInitialize[Ex]所分配的资源。
2、STA中的线程需要消息循环
我们前面说过,对于STA中的调用总是被传递到STA中线程。这个传递的过程
是COM将消息分派到STA中的隐藏窗口来完成的,那么如果线程没有消息循环的话,调
用就会在RPC通道中消失或者堆积如山。
3、不要跨Apartment传递未经调度的接口指针
为什么要这样我就不多说了,我只是简单的说说怎样调度接口指针,有两种
比较常用的方法:一是利用API函数,CoMarshalInterThreadInterfaceInStream和
CoGetInterfaceAndReleaseStream就是这样一对函数,它们会自动地创建Proxy/Stub
对来调度接口指针。二是利用GIT,所谓GIT就是全局接口表,GIT属于进程一级,希望
别的进程使用它的接口指针的线程调用IGlobalInterfaceTable::
RegisterInterfaceInGlobal来把接口指针登记在GIT中,这样任何想使用此接口指针的
线程调用IGlobalInterfaceTable::GetInterfaceFromGlobal获取此接口指针,COM在其
中扮演的角色就是做了第一种办法所作的事。
【服务器端】
1、对于ThreadingModel=Apartment的对象要保护共享数据
有人存在这样一个误解,就是只要声明了ThreadingModel=Apartment之后,开
发者就再也不要担心线程安全性了,其实声明ThreadingModel=Apartment是开发者向COM
保证对象实例对于共享数据的访问是线程安全的,也就是说开发者利用一些同步机制来
保证了共享数据的安全。因为虽然每一个STA中的方法访问都经过了同步,但是如果在不
同的Apartment中实例化了对象,而这些对象实例都要访问共享数据,所以说肯定要对这
些访问数据的动作进行同步。
2、ThreadingModel=Free或者ThreadingModel=Both的对象要保证是线程安全的
前面对于这个问题已经说了很多,就不再多说了。
3、不要在ThreadingModel=Free或者ThreadingModel=Both的对象上使用TLS
我们知道TLS是只有本线程才能够访问的数据区域,而ThreadingModel=Free或者
ThreadingModel=Both的Apartment中拥有任意数目的线程,这些线程可以访问Apartment
中的任意对象,矛盾是显而易见的。