套间生成规则
线程在进行大多数COM操作之前,需要先调用CoInitialize或CoInitializeEx。调用CoInitialize告诉COM生成一个STA套间,并将当前的调用线程和这个套间相关联。而调用CoInitializeEx( NULL, COINIT_MULTITHREADED );告诉COM检查是否已经有了一个MTA套间,没有则生成一个MTA套间,然后将那个套间和调用线程相关联。接着在调用CoCreateInstance或CoGetClassObject等创建对象的函数时,创建的对象将以一个特定规则决定和哪个套间相关联(后叙)。这样完成后,就完成了线程、对象和套间的关联(或绑定)。
前面提到的决定对象去向的规则如下。
当是进程内组件时,根据注册表项<CLSID>\InprocServer32\ThreadingModel和线程的不同,列于下表:
创建线程关联的套间种类 |
ThreadingModel键值 |
组件对象最后所在套间 |
STA |
Apartment |
创建线程的套间 |
STA |
Free |
进程内的MTA套间 |
STA |
Both |
创建线程的套间 |
STA |
""或Single |
进程内的主STA套间 |
STA |
Neutral |
进程内的NA套间 |
MTA |
Apartment |
新建的一个STA套间 |
MTA |
Free |
进程内的MTA套间 |
MTA |
Both |
进程内的MTA套间 |
MTA |
""或Single |
进程内的主STA套间 |
MTA |
Neutral |
进程内的NA套间 |
进程内的主STA套间是进程中第一个调用CoInitialize的线程所关联的套间(即进程中的第一个STA套间)。后面说明为什么还来个进程内的主STA套间。
当是进程外组件时,由主函数调用CoInitializeEx或CoInitialize指定组件所在套间,与上面的相同,CoInitialize代表STA,CoInitializeEx( NULL, COINIT_MULTITHREADED );代表MTA,没有NA。因为NA是COM+提供的,而COM+服务只能提供给进程内服务器,因此只使用上面的注册表项的规则决定DLL组件是否放进NA套间,而没有提供类似CoInitializeEx( NULL, COINIT_NEUTRAL );来处理EXE组件。而且如果可以使用CoInitializeEx( NULL, COINIT_NEUTRAL );将导致调用线程和NA套间相关联了,违背了NA的线程模型,这也是为什么ThreadingModel键在<CLSID>\InprocServer32键下。
跨套间调用 STA线程1创建了一个STA对象,得到接口指针IABCD*,接着它发起STA线程2,并且将IABCD*作为线程参数传入。在线程2中,调用IABCD::Abc()方法,成功或者失败天注定。由于线程2所在的STA套间不同于线程1所在的STA套间,这样线程2就跨套间调用另一个套间的对象了。按照前述的STA规则,IABCD::Abc()应该被转成消息来发送,而如果如上面做法,可以,编译通过,不过运行就不保证了。
COM之所以能够实现前面所说的那些规则(STA、MTA、NA),是因为跨套间调用时,被调用的对象指针是指向一个代理对象,不是组件对象本身。而那个代理对象实现前述的那三个实现算法(转成消息发送,线程切换等),而一般所说的代理/占位对象(Proxy/Stub)等其实都只是指进行汇集工作的代码(后述)。而按照上面直接通过线程参数传入的指针是直接指向对象的,所以将不能实现STA规则,为此COM提供了如下两个函数(还有其他方式,如通过全局接口表GIT)来方便产生代理:CoMarshalInterface和CoUnmarshalInterface(如果在同一进程内的线程间传递接口指针,则可以通过这两个函数来进一步简化代码的编写:CoMarshalInterThreadInterfaceInStream和CoGetInterfaceAndReleaseStream)。
现在重写上面代码,线程1得到IABCD*后,调用CoMarshalInterface得到一个IStream*,然后将IStream*传入线程2,在线程2中,调用CoUnmarshalInterface得到IABCD*,现在这个IABCD*就是指向代理对象的,而不是组件对象了。
因此,前面所说过的所有线程模型的算法都是通过代理对象实现的。要跨套间时,使用CoMarshalInterface将代理对象的CLSID和其与组件对象建立联系的一些必要信息(如组件对象的接口指针)列集(Marshaling)到一个IStream*中,再通过任何线程间通信手段(如全局变量等)将IStream*传到要使用的线程中,再用CoUnmarshalInterface散集(Unmarshaling)出接口以获得指向代理对象的接口指针。因此之所以要获得代理对象的指针是因为想使用COM提供的线程模型(但在COM+中,这不是唯一的理由),如果不想使用大可不必这么麻烦(不过后果自负),并没有强制要求必须那么做。
当线程1和线程2都是MTA时,则可以像最开始说的那样,直接传递IABCD*到线程2中,因为MTA线程模型同意多个线程同时直接调用对象,线程1和线程2在同一个MTA套间中,而那个对象通过某种形式(如ThreadingModel = Free)向COM声明了自己支持MTA线程模型。
而当a.exe的线程1和b.exe的线程2都是MTA时,则依旧需要像上面那样进行接口指针的汇集(列集→传输→散集这个过程)以得到指向代理而非对象的指针,即使线程1和线程2都是在MTA套间中,却是在两个不同的MTA套间中,因此是跨套间调用,需要汇集操作。
汇集代码
前面已经说明了套间的规则都是通过对代理对象而非组件对象发起调用以截取对组件对象的调用由代理对象来实现的。代理对象要和组件对象交互,将方法参数传递给组件对象,需要使用到汇集技术,也就是列集→传输→散集这个过程。
列集(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套间正确工作)。