COM线程模型(七)——the end

本文详细阐述了COM组件对象模型中的线程模型概念,包括STA、MTA、NA等套间的工作原理及其对组件和代理对象的影响。同时,介绍了如何在不同线程模型下正确实现组件,以确保线程安全和高效运行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

汇集代码

前面已经说明了套间的规则都是通过对代理对象而非组件对象发起调用以截取对组件对象的调用由代理对象来实现的。代理对象要和组件对象交互,将方法参数传递给组件对象,需要使用到汇集技术,也就是列集→传输→散集这个过程。

列集(Marshaling)指将信息以某种格式存为流(IStream*)形式的操作;散集(Unmarshaling)则是列集的反操作,将信息从流形式中反还出来;传输则只是流形式的传递操作。

这里经常发生误会。前面的CoMarshalInterface所做的列集,是将代理对象的CLSID及一些持久信息(用于初始化代理对象)格式化为一种格式(网络数据描述——Network Data Representation)后放到一个流对象中,可以通过网络(或其他方式)将这个流对象传递到客户机,由客户通过CoUnmarshalInterface从传来的流对象中反还出代理对象的CLSID和初始化用的一些持久信息,生成代理对象并使用持久信息初始化它以用于汇集操作。这就是发生误会的地方——这里的汇集操作不同于上面的汇集操作,其汇集的是接口方法的参数而不是什么CLSID和一些初始化信息。

因此CoMarshalInterface和CoUnmarshalInterface是用于汇集接口指针的,再准确点应该是用于生成代理对象的。代理对象应由读者自己实现,用于汇集接口方法的参数。一般有两种代理对象的实现方式:自定义汇集和标准汇集。

对于自定义汇集,组件需实现IMarshal接口和一个代理组件(即完全实现真正组件所有接口的一个副本,实现了汇集方法参数及线程模型的规则,也必须实现IMarshal接口),并将这个代理组件在客户机上注册,以保证代理对象的正确生成。注意:如果参数中有接口指针,必须用CoMarshalInterface和CoUnmarshalInterface进行汇集,否则无法实现正确的线程模型,且代理组件是线程模型的实现者,这点组件必须自己保证(如发送消息等)。

对于标准汇集,组件无需实现IMarshal接口及代理组件,代替的,组件则需要为自己生成一个代理/占位组件(Proxy/Stub),其由于可通过MIDL由IDL文件自动生成,效率高,代码的正确性有保证,因而被鼓励使用。COM提供了一个标准代理对象的实现,其通过聚合组件的代理/占位组件以表现出其好像是组件的代理对象。与自定义汇集一样,需要将这个代理/占位组件在客户机上注册以保证代理对象的正确生成。

至于这两种汇集的具体工作机理,由于与本文无关,在此不表,这里仅仅只为消除代理对象和代理/占位组件之间的混淆。

注意:对于将运行于NA套间的组件,由于COM+的强制要求,其必须使用标准汇集进行代理对象的生成而不是自定义汇集(COM+运行时期库重写了标准代理对象来截获对组件对象的调用和其自身的某些特殊处理——如保证NA套间正确工作)。

套间实现规则

  如前面所说,COM的套间机制要成功,必须服务器(组件)、客户和COM运行时期库三方面合力实现,其中有任何一方不按着规矩来,将不能实现套间机制的功能,不过这并不代表什么错误,套间机制不能运作并不代表程序会崩溃,只是不能和其他COM应用兼容而已。

比如:对象中的属性1在设计的算法中肯定不会被两个以上的线程写入,只是会被多个线程同时读出而已,因此不用同步,可以用MTA,但对象的属性2却可能被多个线程写入,因此决定使用STA。从而在客户端,通过前面说的CoMarshalInterface和CoUnmarshalInterface将对象指针传到那个只会写入对象的属性1的线程,其实这时就可以直接将对象指针传到这个线程,而不用想上面那样麻烦(而且增加了效率),但是就破坏了COM的套间规矩了——两个线程可以访问对象,但对象在STA套间中。所以?!!什么事都不会发生,因为已经准确知道这个算法不会捅娄子(线程访问冲突),即使破坏COM的规矩又怎样?!而且组件仍可以和其他客户兼容,因为不按规矩来的是客户,与组件无关。不过如果组件破坏规矩,那么它将不能和每一个客户兼容,但并不代表它和任何客户都不兼容。这里其实就是客户和组件联合起来欺骗了COM运行时期库。

上面的例子只是想帮助读者加深对套间的理解,实际中应该尽量保持和COM规范的兼容性(但不兼容并不代表是错误的)。客户要做的工作前面已经说过了(那两个函数或全局接口表或其他只要正确的方式),下面说明组件应该做的工作。组件可以存在于四个套间中(多了一个主STA套间),所需工作分别如下:

STA 当一个组件是STA时,它必须同步保护全局变量和静态变量,即对全局变量和静态变量的访问应该用临界段或其他同步手段保护,因为操作全局和静态变量的代码可以被多个STA线程同时执行,所以那些代码的地方要进行保护。比如对象计数(注意,不是引用计数),代表当前组件生成的对象个数,当减为零时,组件被卸载。此变量一般被类厂对象使用,还好ATL和MFC已经帮我们实现了缺省类厂,这里一般不用担心,但自定义的全局或静态变量得自己处理。

主STA 与STA唯一的不同是这是傻瓜型的,连静态和全局变量都可以不用线程保护,因为所有不是安全访问静态和全局变量的对象都通过主线程(第一个调用CoInitialize的线程)的消息派送机制运行,因此不安全的访问都被集中到了一个线程的调用中,因而调用被序列化了,也就实现了对静态和全局变量的线程保护。至于为什么是主线程,因为进程要使用STA,则一定会创建主线程,所以一定可以创建主STA。因此主STA并不是什么第四种套间,只是一个STA套间,不过关联的是主线程而已,由于它可以被用作保护静态和全局变量而被单独提出来说明。因此一个进程内也只有一个主STA套间。

MTA 必须对组件中的每个成员和全局及静态变量的访问使用同步手段进行保护,还应考虑线程问题,即不是简单地保护访问即可,还应注意线程导致的错误的操作,最经典的就是IUnknown::Release()。


DWORD IUnknown::Release()
{
DWORD temp = InterlockedDecreament( &m_RefCount );
if( !temp ) // 不能用m_RefCount,原因请自己思考
delete this; // 因此不是只要用原子访问函数保护了m_RefCount的访问就行了
return temp; // 前面对全局变量的保护也和此类似,要考虑线程问题
}

如果读者对自己多线程编程的技术没有信心,建议最好不要编写可以存在于MTA套间的组件,不过就不能获得MTA的高性能了。

在编写MTA时还应该注意到线程亲缘性(thread affinity)。没有线程亲缘性是指没有任何线程范围的成员变量,比如线程局部存储(TLS)、窗口句柄等。也就是说在MTA中不能保存任何记录着TLS内存的指针或窗口句柄,如果保存将没有意义(比如A线程记录的内存空间对B线程来说是无效的,因为TLS构造了一个线程相关的内存空间,就像每个进程都有自己的私有空间)。而不幸地MFC在它的底层运作机制的实现中大量使用了TLS,如模块线程状态、线程状态等。正是由于这个原因,MFC不能编写在MTA中运行的组件。

NA 由于可能会多个线程同时访问NA套间的对象,因此和MTA一样,其不能有线程亲缘性并需要保护每个成员和全局及静态变量。而关于NA的轻量级代理,是由COM+运行时期库生成的,读者完全不用操心(只需将那个组件的ThreadingModel键值赋值为“Neutral”即可)。

前面提到过有一种进程内组件的ThreadingModel键值可以被赋为“Both”,这种组件很像NA,哪个套间都可能直接访问它,但只是可能,而NA组件是可以,这点可以从前面的那个进程内组件所属套间的规则表中看出。这种组件可以支持一种称作自由线程汇集器(FTM——Free Threaded Marshaler)的技术,由于其与本文题目无关,在此不表。当Both的组件使用了自由线程汇集器时,除了满足MTA的要求以外(上面所说的线程安全保护和没有线程相关性),还要记录传进来的接口指针的中立形式(比如IStream*,通过CoMarshallInterface得到),以防止对客户的回调问题。

最后只是提醒一下,有3个STA套间,STA1、STA2和STA3。STA1用CoMarshallInterface得到的IStream*传到STA2中通过CoUnmarshalInterface得到的代理和在STA3中同样通过CoUnmarshalInterface得到的代理不同,不能混用。因为当STA2和STA3调用在STA1的对象时,STA1如果回调(连接点技术就是一种回调)调用者,则STA2和STA3的代理能分别正确的指出需要让哪个线程执行回调操作,即向哪个线程发送消息,因此不能混用。


如果您对此文章有任何疑问,欢迎到 流媒体开发论坛提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值