之前看了COM本质论,翻了几遍,对COM套间似乎理解了,但总有种说不清楚的感觉,隐隐的觉得理解的不够清楚。前几天查了资料,用代码验证了套间类型及线程分配情况之后,终于对套间有了清楚一些的认识,这里首先引用一篇对COM套间讲解的译文,接下来在讲述通过代码来更好的理解它。 原文: http://www./cpp/com-tech/activex/apts/article.php/c5529/Understanding-COM-Apartments-Part-I.htm 译文: http://blog.sina.com.cn/s/blog_56dee71a0100nt08.html 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。调用这两个函数时,线程将被放入到套间中。放入到什么类型的套间决定于线程调用哪个函数以及如何调用。 *如果线程调用CoInitializeEx并且传递参数COINIT_APARTMENTTHREADED,则线程也被放入到一个STA中:
*如果调用CoInitializeEx并且传递参数COINIT_MULTITHREADED,则线程被放入到进程里唯一的MTA中: 从大的范围来看,进程的套间配置取决于进程中的线程如何调用CoInitialize[Ex]。存在不调用CoInitialize[Ex]函数而COM创建套间的情况,但是为了让问题简单,暂时不讨论这种情况(but 作为一个例子,假设启动了一个新进程,进程中的线程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套间绝对是必要的。在下一部分你会明白我的意思的。
本文的前一部分阐述了为什么和怎样使用COM套间。读过之后,你会知道,调用CoInitialize或者CoInitializeEx的时候,线程被放入到套间中。你还会知道,对象创建的时候也被放入到套间中,COM使用注册表中的ThreadingModel值决定将进程内对象放到什么类型的套间中。 你还会知道,有三种类型的套间:单线程套间STA;多线程套间MTA;线程中立套间NTA。Windows l * l * l* 上个月的文章《深入理解IUnknown(Into 1 要编写可以工作的COM客户端,需要遵循三条规则。牢记这些规则,你就可以在编写COM客户端时避免严重的错误。 规则1:客户线程必须调用CoInitialize[Ex] 线程做任何与COM相关的操作之前,必须调用CoInitialize或者CoInitializeEx初始化COM。如果客户程序有20个线程,其中10个使用COM,则这10个线程都应该调用CoInitialize或者CoInitializeEx。调用线程将在这两个API中被分配给一个套间。对于没有分配给套间的线程,COM是无法施行并发规则的。此外还要记住,成功调用了CoInitialize或者CoInitializeEx的线程应该在终止前调用CoUninitialize。否则,由CoInitialize[Ex]分配的资源将直到进程终止才释放。 这条规则看起来很简单,只是一个函数调用而已。但是你会惊奇地发现,这条规则经常被违背。违背这条规则的错误一般在调用CoCreateInstance或者其他COM 具有讽刺意味的是,有时候开发者不调用CoInitialize[Ex]的原因是,微软告诉他们不需要调用。MSDN中有篇文章说COM客户端有时候可以避免调用这个函数。但文章随后说这可能会导致拒绝访问。我近期收到一个开发者的电话,说客户线程调用Release的时候会死锁或者发生拒绝访问异常。原因是?有些线程没有调用CoInitialize[Ex]就发起方法调用了,结果调用Release的时候发生问题了。幸运地是,解决问题只需要简单地加几个CoInitialize[Ex]调用。 记住:调用CoInitialize[Ex]总是没有坏处的。对于调用COM 规则2:STA线程需要消息循环 如果不理解单线程套间机制,这条规则看起来不那么明显。客户调用基于STA的对象时,调用将被传递到STA中运行的线程。COM通过向STA的隐藏窗口投递消息来完成这种传递。那么,如果STA中的线程不接收和分发消息将发生什么?调用将在RPC通道中消失,永远也不返回。它将永远凋谢在STA的消息队列中(It 开发者问我为什么方法调用不返回的时候,我首先问他们“你调用的对象是在STA中吗?如果是,驱动STA的线程是否有消息循环?”。多半的回答是“我不知道”。如果你不知道,你就是在玩火。调用CoInitialize,或者使用参数COINIT_APARTMENTTHREADED调用CoInitializeEx,或者调用MFC的AfxOleInit的时候,线程被分配到一个STA中。如果随后在这个STA中创建对象,而STA线程又没有消息泵,那么对象不能接收来自其他套间的客户的方法调用。消息泵可以这样简单: 如果缺少这些简单的语句,把线程放入STA时要当心。一个常见的情况是MFC应用程序启动工作线程(MFC工作线程的定义是,缺少消息泵的线程),而线程调用AfxOleInit将自身放入到STA中。如果STA不容纳任何对象,或者虽然容纳对象但是却没有来自其他套间的客户,你不会遇到问题。但是如果STA容纳导出接口指针到其他套间的对象,则对这些接口指针的调用将永远不会返回。 规则3:不要在套间之间传递原始未列集的接口指针 设想编写一个有两个线程的COM客户端。两个线程都调用CoInitialize进入一个STA,然后其中一个线程——线程A,使用CoCreateInstance创建一个COM对象。线程A想要与线程B共享从CoCreateInstance返回的接口指针。所以线程A将接口指针赋值给一个全局变量,然后通知线程B指针已经准备好了。线程B从全局变量读取接口指针并且对对象发起调用。这个过程有什么错误吗? 这个过程会引发事故。问题是线程A向其他套间中的线程传递了原始未列集的接口指针。线程B应该只通过列集到线程B所属套间的接口指针与对象通信。 这里“列集(Marshaling)”的意思是给COM在线程B所属套间中创建新代理的机会,让线程B可以安全地进行调用。在套间之间传递原始接口指针的后果可以从与时间极其相关(也很难重现)的数据损坏到完全死锁。 如果线程A列集接口指针,则可以安全地与线程B共享接口指针。COM客户端有两种基本的方法将接口指针列集到其他套间: l * 线程A调用CoMarshalInterThreadInte l * GIT是每个进程一个的表格,让各个线程可以安全地共享接口指针。如果线程A想要与同一个进程中的其他线程共享接口指针,可以使用IGlobalInterfaceTable::RegisterInterfaceInGloba 有没有不列集需要与其他线程共享的接口指针也OK的情况?有。如果两个线程属于同一个套间,则可以共享原始未列集的接口指针,而这只可能在两个线程都属于MTA时发生。如果不确定是否需要,请进行列集。调用CoMarshalInterThreadInte 2 编写COM服务器时也应该遵守一些规则。 规则1:保护ThreadingModel=Apartment的对象的共享数据 标记对象的ThreadingModel=Apartment就可以不考虑线程安全问题?这是关于COM编程的一个最常见的错误想法。注册进程内对象的ThreadingModel=Apartment暗示COM,对象(以及从DLL创建的其他对象)会以线程安全的方式访问共享数据。这意味着已经使用临界区或者其他线程同步原语来保证在任何时刻只有一个线程可以接触到共享数据。对象之间数据共享通常有三种方式: l * l * l * 为什么线程同步对于ThreadingModel=Apartment的对象是很重要的?考虑从同一个DLL创建两个对象A和B的情况。假定两个对象都读写在DLL中声明的一个全局变量。因为标记为ThreadingModel=Apartment,对象可能分别在不同的STA中创建和运行,因此,也是在不同的线程中运行。但是两个对象访问的全局变量是共享的,只在进程内实例化一次。如果来自A和B的调用几乎同时发生,而且A写入那个变量,B读取那个变量(或者相反),那么变量可能被破坏,除非串行化线程的操作。如果不提供同步机制,那么多数时候会遇到问题。最终两个线程可能在共享数据上发生冲突,后果无法预知。 存在不需要同步机制就可以安全地访问共享数据的情况吗?存在。下列条件下可以不需要同步机制: l * l * l * 对于除此之外的情况,要确保ThreadingModel=Apartment的对象以线程安全的方式访问共享数据,只有这样才是正确完成了任务。 规则2:标记为ThreadingModel=Free或者ThreadingModel=Both的对象应该是线程安全的。 标记对象是ThreadingModel=Free或者ThreadingModel=Both时,对象将被或者可能被放入到MTA中。记住:COM不会串行化对基于MTA的对象的调用。因此,毫无疑问地(beyond 规则3:避免在标记为ThreadingModel=Free或者ThreadingModel=Both的对象里使用线程局部存储(TLS) 一些Windows程序员使用线程局部存储临时保存数据。设想在实现一个COM方法时,需要缓存一些关于当前调用的信息,以备下次调用时使用。这时你可能很想使用TLS。在STA中,这样做没问题。但是如果对象在MTA中,就应该像躲避瘟疫那样避免使用TLS。 为什么?因为进入MTA的调用被传递给RPC线程。每次调用可能被传递给不同的RPC线程,即使调用都是来自于同一个线程中的同一个调用者。一个线程不能访问另一个线程的线程局部存储。所以如果调用1到达线程A,对象将数据保存在TLS中;然后调用2到达线程B,对象试图取出在调用1中存入TLS的数据时,会找不到数据。这个道理很简单。 对于基于MTA的对象,在方法调用之间使用TLS缓存数据时要注意,这种方法只在所有的方法调用来自于对象所在的MTA中的同一个线程时才可以正确工作。 你在开玩笑? 我应该严肃对待这些规则吗?一点没错。我在COM应用程序中发现的bug大约有一半是因为违背本文描述的规则而导致的。即使你不理解这些规则,也请遵守它们,这样你的世界才会是美好的。
以上内容全为复制粘贴,但也费了半天劲呢。虽然文章讲述的很好,但如果对COM套间没有一定的理解基础的话,可能读了之后并不一定能很清楚的理解,毕竟这里讲述的只是原理,而并没有具体的实例,接下来我通过代码实例来一块理解COM套间。 |
|
来自: legionDataLib > 《COM/ATL》