扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
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这几类套间。敬请关注。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者