![此博文包含图片](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
COM引入了一种并发机制,可以截获并串行化对于设计为只能一次处理一个方法调用的对象的并发调用。这种机制以称为“套间(apartments)”的抽象边界概念为中心。在解决不能正确工作的COM系统的问题时,我发现大约40%问题的原因是缺乏对于套间的理解。这种知识的缺乏并不意外,因为套间是COM中最复杂的领域,而且也没有很好地文档化。微软的目的是好的,但是在Windows
本文是一个两部分系列文章的第一部分。系列文章将解释什么是套间、套间存在于什么地方,以及如何避免套间引入的问题。文章的第一部分将介绍COM基于套间的并发机制;第二部分将介绍一些规则,以避免隐藏而又令人讨厌的Bug。
1
套间是一个并发边界,一个在对象和客户线程之间的假想的盒子,用以隔离具有不兼容线程特性的COM客户和COM对象。套间存在的主要目的是让COM可以串行化对于非线程安全对象的方法调用。如果没有告诉COM对象是线程安全的,则COM不会允许多个调用同时到达对象。相反地,如果告诉COM对象是线程安全的,则COM会让对象处理多个线程中的并发调用。
每个使用COM的线程,以及这些线程创建的每个对象,都被分配到某个套间中。套间不能跨越进程边界,所以如果对象和其客户位于不同的进程中,则它们也在不同的套间中。客户创建进程内对象的时候,COM必须决定将其放在创建者套间中,还是放在客户进程中的另一个套间里。如果COM将对象和创建对象的线程放在同一个套间中,则客户将直接访问对象。如果COM将对象放在另一个不同的套间中,则创建对象的线程对对象的调用将被列集(marshaled)。
图1展示了线程和对象共享套间,以及线程和对象位于不同套间时二者的关系。线程1中的调用将直接访问线程创建的对象;线程2中的调用将通过代理(proxy)和桩基(stub)进行。COM在将接口指针列集到线程2的套间时创建代理/桩基对。跨越套间边界传递接口指针时必须对指针进行列集,这是一条规则。这意味着,如果涉及到定制接口,即使是进程内对象,如果需要与位于其他套间中的客户通信,也还是需要提供与跨进程和跨机器方法调用时一样的,用于列集支持的代理/桩基DLL(或者选择类型库列集时的类型库)。
图1:对其他套间中对象的调用被列集,即使对象和调用者属于同一个进程
Windows
l
l
l
每个线程只能有一个单线程套间,但是可以容纳的对象个数是无限的。而且,COM不限制进程中STA的个数。进程中第一个创建的STA称为进程的主STA。对STA中对象的调用在投递前都先传递到STA线程中。因为所有对对象的调用都在同一个线程中执行,所以基于STA的对象不能同时执行多个调用。COM使用STA来串行化对于非线程安全对象的调用。如果不明确告诉COM对象是线程安全的,则COM将把对象放到STA中,从而让对象不会被并发访问。
关于STA操作的一个有趣方面是:COM如何将对基于STA的对象的调用传递到STA线程中。COM创建STA的时候,会同时创建一个隐藏窗口,这个窗口的窗口过程知道如何处理代表方法调用的私有消息。目标是STA的方法调用离开COM的RPC通道时,COM将向STA窗口投递一个代表这个调用的消息。STA中的线程收到消息后,将消息分发到隐藏窗口,隐藏窗口的窗口过程将调用投递给桩基,桩基将最终执行对对象的调用。因为线程一次只能接收、分发和处理一个消息,STA自然而有效地成为调用串行机制。如图2所示,如果同时发起n个对基于STA的对象的调用,则调用将被排队,然后依次投递给对象。
图2:进入STA的调用被转化为消息,投递到消息队列。消息队列中的消息被STA中运行的线程依次转化回方法调用。
调用离开STA时的情形也同样重要。COM不能让线程阻塞在RPC通道中,因为回调可能导致死锁:想象一下当STA线程调用其他套间中的对象,而这个对象反过来调用STA中的另一个对象时会发生什么。如果STA线程阻塞在RPC通道中,则调用永远不会返回,因为唯一一个可以处理回调的线程正在RPC通道中等待最初的调用返回。因此,调用离开STA时,COM会阻塞STA线程,但是让STA线程仍然可以处理回调。为了让回调可以发生,COM会跟踪每个方法调用的因果关系,以便能够识别何时应该释放正在RPC通道中等待某方法调用返回的STA线程,让其处理另一个进入的调用。默认情况下,STA入口有调用到达时,如果STA线程正在等待出调用返回,而且到达的入调用与正在等待返回的出调用不属于同一个因果链,则到达的入调用将阻塞。编写消息过滤器可以改变这个默认行为,下一部分对此进行讨论。
多线程套间完全不同。COM限制每个进程只能有一个MTA,但是没有限制MTA中线程和对象的个数。MTA没有隐藏窗口和消息队列。对MTA中对象的调用被随机地传递给RPC线程池里的线程,不会串行化(见图3)。这意味着MTA中的对象最好是线程安全的,因为没有外部机制保证基于MTA的对象一次只接收一个调用,对象可能被不同的RPC线程并发地调用。
图3:进入MTA的调用被传递到RPC线程而不会被串行化
对于离开MTA的调用,COM不会进行特别处理。调用线程可以阻塞在RPC通道中,如果发生回调,不会产生死锁,因为回调会被传递给另一个RPC线程。
Windows
2如何为线程分配套间
以任何方式使用COM的线程必须首先调用CoInitialize或者CoInitializeEx初始化COM。调用这两个函数时,线程将被放入到套间中。放入到什么类型的套间决定于线程调用哪个函数以及如何调用。
l
l
l
作为一个例子,假设启动了一个新进程,进程中的线程1调用CoInitialize:
随后,线程1启动线程2,3,4和5,这些线程使用下列语句初始化COM:
图4展示了最终的套间配置。线程1,2和5被分配到单独的STA中,因为每个STA中只能有一个线程。另一方面,线程3和4被分配到进程的MTA中。记住:COM不会为进程创建多个MTA,而是在唯一的一个MTA中放置任何数量的线程。
图4:进程有五个线程,分布于三个STA和一个MTA中。
如果你喜欢刨根问底(if
3
现在该介绍如何为对象分配套间了。COM用于决定在哪个套间中创建对象的算法对于进程内对象和进程外对象是不同的。进程内对象更有趣,因为只有进程内对象是可以创建于创建者套间中的。我们首先讨论进程内对象,然后讨论进程外对象。
COM通过从注册表中读取对象的ThreadingModel值来决定在哪个套间中创建进程内对象。ThreadingModel是分配给用以标识对象DLL的InprocServer32子键的命名值。下面以REGEDIT格式显示的注册表条目标识了CLSID为99999999-0000-0000-0000-111111111111、DLL为MyServer.dll,ThreadingModel为Apartment的对象:
Apartment是Windows
Apartment
COM尽量放置进程内对象到创建者线程所属的套间中。比如说,如果STA线程创建标识为ThreadingModel=Apartment的对象,COM将在创建者线程的STA中创建对象。如果MTA线程创建ThreadingModel=Free的对象,COM将会把对象放置在MTA中。然而,有时候COM不能将对象放置在创建者的套间中。比如说,如果STA线程创建标识为ThreadingModel=Free的对象,则对象将在进程的MTA中创建,创建者线程将通过代理和桩基访问对象。类似地,如果MTA线程创建ThreadingModel=None或者ThreadingModel=Apartment的对象,则来自创建者线程的调用将从MTA列集到对象的STA中。下表显示了STA和MTA线程创建具有任何有效的ThreadingModel值(或者没有ThreadingModel值)的对象时的情况:
为什么ThreadingModel=None限制对象在进程的主STA中?因为只有这样COM才能在不知道对象是否是线程安全时让多个对象安全地执行。假设从同一个DLL创建两个ThreadingModel=None的对象。如果这两个对象访问DLL中的任何全局变量,则COM必须在相同线程中执行所有对这两个对象的调用,否则两个对象可能试图同时读或者写同一个全局变量。限制对象在主STA中就是COM让对象在相同线程中执行的方式。
线程模型对于编码有重要的指导意义。比如说,标记为ThreadingModel=Free或者ThreadingModel=Both的对象必须是线程安全的,因为对基于MTA的对象的调用时不会被串行化。即使是ThreadingModel=Apartment的对象,也应该是部分线程安全的,因为ThreadingModel=Apartment不能阻止从同一个DLL创建多个对象,从而在共享数据上发生冲突。本文的下一部分将讨论这个问题。
4
进程外对象没有ThreadingModel值,因为COM使用完全不同的算法为进程外对象分配套间。简而言之,COM将进程外对象放到与创建对象的服务器进程相同的套间中。大多数进程外(EXE)COM服务器以调用CoInitialize或者CoInitializeEx将主线程放入到STA中开始。然后服务器为其可以创建的对象类型创建类对象并使用CoRegisterClassObject进行注册。激活请求到达以这种方式初始化的服务器时,请求在进程的STA中被处理。结果,服务器进程创建的对象也在进程的STA中。
可以将进程外对象移动到MTA中,只要将注册类对象的线程放置在MTA中就可以了。这样进入的激活请求会到达在服务器进程的MTA中执行的RPC线程。为响应请求创建的对象也将位于MTA中。
要点是,在EXE类型的COM服务器里,调用CoRegisterClassObject的线程所在的套间,也是服务器创建的对象所在的套间。当然也存在例外:使用ATL的CComAutoThreadModule和CComClassFactoryAutoThre
继续下一部分
这就完了?本文展示的很多细节看起来很神秘,没什么实际价值。然而,要避免大多数常见的、折磨着COM程序员的危险陷阱,理解COM套间绝对是必要的。在下一部分你会明白我的意思的。