COM的线程模型

 前段时间碰到一个奇怪的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  
中的任意对象,矛盾是显而易见的。  

你可能感兴趣的:(COM的线程模型)