首先,只要遵守COM规范,不用COM库也能编写COM程序,那相当于自己实现用到的COM库函数。本篇COM如果单独出现,指COM库。
COM线程模型-套间
来源: http://blog.csdn.net/crybird/archive/2008/10/11/3057067.aspx
查找了好多资料,终于对套件这一概念有一点心得,赶紧记录下来。
首先,只要遵守COM规范,不用COM库也能编写COM程序,那相当于自己实现用到的COM库函数。本篇COM如果单独出现,指COM库。
1 进程、线程回顾
《WINDOWS核心编程》对进程和线程有深入解释,一个程序运行起来,需要一个进程作为容器。进程管理所有系统资源、内存、线程等等。线程是CPU的调度单位,有自己的栈和寄存器状态。程序最初创建的线程叫主线程,主线程可以创建子线程,子线程还可以创建子线程。
不同进程之间是无法直接通信的,因为它们在虚拟内存中的地址不一样。但操作系统通过LPC机制,可以完成不同进程之间的通信。
COM在进程间通信的方法是本地过程调用(LPC),因为操作系统知道各个进程的确切逻辑地址,所以操作系统可以完成这一点。不同进程间传递的参数需要调整,LPC技术可以完成普通数据的直接拷贝(甚至包括自定义类和指针),但对于接口参数,COM实现了IMarshal接口以调整。
为了可以用同样的方式和进程外、远程组件通信,客户端不直接和组件通信,而是和代理/存根通信,代理/存根是(而且必须是) DLL形式,能完成参数调整和LPC调用。代理存根不用自己写,系统会自动产生。
注:接口的调整,包括列集和散集两种marshal/unmarshal。
2 COM线程模型
2.1 分清模型与实现
看过《Inside C++ Object Model》(中文名:深入C++对象模型;侯捷译)的人都知道,C++对象模型有三种,各家编译器都选择其中效率最高的一种实现出来。另外两种就留在了理论世界,实现出来没有太大意义。提这个的原因,就是为了弄清楚这一点:COM线程模型只是理论构想,是一种抽象的数学模型,还要COM库通过各种手段实现出来,才能为我们使用。
2.2 套间的由来
最开始的COM库,支持的使用组件的唯一模式是single-thread-per-process模式。这样就避免了多线程的同步,而且组件执行的线程肯定是创建它的线程。
然而组件对象真正的执行环境很复杂。COM组件的执行环境有两种:单线程环境Single-Thread,多线程环境Multi-Thread。单线程要考虑执行线程是否是创建组件的线程;多线程还要考虑并发、同步、死锁、竞争等问题。无论哪种环境,都要编写大量的代码以使COM组件对象正确的运行。
为了使程序员减轻痛苦,COM库决心提供一套机制来帮助程序员。如果我们都遵从这套机制,只要付出较少的劳动,就可以让组件对象和COM库一起完成工作。COM库这套机制的核心技术就是“套间技术”。
2.3 COM的多线程模型
2.3.1 COM库的规定
关于多线程问题方面,COM库做出了如下规则(不是COM标准,是COM库为了简化多线程编程中对组件的调用而制定的):
1. COM库提供两种套间,单线程套间和多线程套间,COM组件的编写者最好提供对应的属性(后面会提到),COM组件的使用者要在套间里创建和调用组件。
2. COM库对所有的调用进行参数调整(如果需要),不管是对进程内服务器的调用,还是对进程外服务器的调用。
3. 线程内调用、跨线程调用、跨进程调用都用统一的方式。需要用代理的会用代理。
如此COM规定了COM库、组件编写者、组件使用者三方合作关系。COM库进行协调关系,会根据组件的能力,在不同环境(套间)中创建和调用组件;编写者要说明组件可以生存的环境;调用者查询接口,合理调用。
2.3.2 单线程套间STA
Single-threaded Apartments,一个套间只关联一个线程,COM库保证对象只能由这个线程访问(通过对象的接口指针调用其方法),其他线程不得直接访问这个对象(可以间接访问,但最终还是由这个线程访问)。
COM库实现了所有调用的同步,因为只有关联线程能访问COM对象。如果有N个调用同时并发,N-1个调用处于阻塞状态。对象的状态(也就是对象的成员变量的值)肯定是正确变化的,不会出现线程访问冲突而导致对象状态错误。
注意:这只是要求、希望、协议,实际是否做到是由COM决定的。这个模型很像Windows提供的窗口消息运行机制,因此这个线程模型非常适合于拥有界面的组件,像ActiveX控件、OLE文档服务器等,都应该使用STA的套间。
2.3.3 多线程套间MTA
Multithreaded Apartments,一个套间可以对应多个线程,COM对象可以被多个线程并发访问。所以这个对象的作者必须在自己的代码中实现线程保护、同步工作,保证可以正确改变自己的状态。
这对于作为业务逻辑组件或干后台服务的组件非常适合。因为作为一个分布式的服务器,同一时间可能有几千条服务请求到达,如果排队进行调用,那么将是不能想象的。
注意:这也只是一个要求、希望、协议而已。
2.3.4 COM+新增NA
COM+为了进一步简化多线程编程,引入了中立线程套间概念。
NA/TNA/NTA,Neutral Apartment/Thread Neutral Apartment / Neutral Threaded Apartment。这种套间只和对象相关联,没有关联的线程,因此任何线程都可以直接访问里面的对象,不存在STA的还是MTA的。
2.4 到底什么是套间
根据《COM技术内幕》的观点,COM没有定义自己新的线程模型,而是直接利用了Win32线程,或者说对其做了改造、包装。线程间的同步也是直接用的Win32 APIs。
《COM本质论》设这样定义的:套间定义了一组对象的逻辑组合,这些对象共享一组并发性和冲入限制。每个COM对象都属于某一个套间,一个套间可以包含多个COM对象。
MSDN上解释说,可以把进程中的组件对象想象为分成了很多组,每一组就是一个套间。属于这个套间的线程,可以直接调用组件,不属于这个套间的线程,要通过代理才能调用组件。
最直接的说,COM库为了实现简化多线程编程的构想,提出了套间概念。套间是一个逻辑上的概念,它把Win32里的线程、组件等,按照一定的规则结合在一起,并且以此提供了一种模式,用于多线程并发访问COM组件方面。可以把套间看作COM对象的管理者,它通过调度,切换COM对象的执行环境,保证COM对象的多线程调用正常运行。COM和线程不是包含关系,而是对应和关联关系。
3 第一方COM库:模型的实现
3.1 单线程套间STA
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);这句代码创建了一个STA,然后套间把当前的线程和自己关联在一起,线程被标记为套间线程,只有这个线程能直接调用COM对象。
在创建套间的时候,COM创建了一个隐藏的窗口。关联线程对组件的调用直接通过接口指针调用方法;其他线程对套间里的对象的调用,都转变成对那个隐藏窗口发送消息,然后由这个隐藏窗口的消息处理函数来实际调用组件对象的方法。编写组件代码的时候,只需调用DispatchMessage即可将方法调用的消息和普通的消息区分开来(通过隐藏窗口的消息处理函数)。
由于窗口消息的处理是异步的,所以所有的调用都是依次进行的,不必考虑同步的问题。只要调用的时候,参数进行合理调整即可(COM库会做到这一点)。但是对于全局变量和静态变量,组件编写者还是要费心的。
一个STA只关联一个线程, single-thread-per-process模式只是STA的一个特例。使用这种模式的线程叫套间线程(Apartment Thread)。
3.2 多线程套间MTA
CoInitializeEx(NULL, COINIT_MULTITHREADED);第一次如此调用的时候,会创建一个MTA,然后套间把当前线程和自己关联在一起,线程被标记为自由线程。以后第二个线程再调用(在同一进程中)的时候,这个MTA会把第二个线程也关联在一起,并标记为自由线程。一个MTA可以关联多个线程。
所有的关联线程都可以调用套间中的组件。这就涉及到同步问题,需要组件编写者解决。
一个MTA可以关联一个或多个线程,这种模式下,COM组件自己要考虑同步问题。使用这种模式的这些线程叫做自由线程(Free Thread) 。
3.3 总结
一个进程可以有0个、1个或多个STA,还可以有0个或1个MTA。
一个线程,进入(或创建)套间后,不能改变套间模式;但可以退出套间,然后以另外的模式再进入(或创建)另一个套间。
在一个进程中,主套间是第一个被初始化的。在单线程的进程里,这是唯一的套间。调用参数在套间之间被调整,COM库通过消息机制处理同步。
如果你设计多个线程作为自由线程,所有的自由线程在同一个单独的套间中,参数被直接(不被列集)传递给这个套间的任何线程,而且你要处理所有的同步。
在既有自由线程又有套间线程的进程里,所有自由线程在一个套间里,而其他套间都是单线程套间。而进程是包含一个多线程套间和N个单线程套间的容器。
COM的线程模型为客户端和服务器提供了这样一种机制:让不同的线程协同工作。不同进程内,不同线程之间的对象调用也是被支持的。以调用者的角度来看,所有对进程外对象的调用都是一致的,而不管它在怎样的线程模型。以被调用者的角度来看,不管调用者的线程模型如何,所获得的调用都是一致的。
客户端和进程外对象之间的交互也很直接,即使它们使用了不同的线程模型,因为它们属于不同的进程。COM介入了客户端和服务器之间,通过标准的列集和RPC,并提供了跨线程操作的代码。
4 第二方COM组件的调用者
4.1 各种调用
4.1.1 同一线程中的调用
同步问题:不需要,调用者和组件在同一线程中,自然同步。
调整问题:不需要,COM库不需要任何介入,参数也不需要调整,组件也不必线程安全。
调用地点:当前线程
这是最简单的情况。
4.1.2 套间线程之间的调用
同步问题:COM库对调用进行同步。
调整问题:不管两个套间是否在同一进程,都需要调整。某些情况下,需要手动调整。
调用地点: 对象所在套间线程。
4.1.3 自由线程之间的调用
同步问题:COM不进行同步,组件自己同步。
调整问题:同一进程不调整,不同进程要调整。
调用地点:客户线程。
4.1.4 自由线程调用套间线程的组件
同步问题:COM库对调用进行同步。
调整问题:不管两个套间是否在同一进程,都需要调整。某些情况下,需要手动调整。
调用地点:套间线程
4.1.5 套间线程调用自由线程的组件
同步问题:COM不进行同步,组件自己同步。
调整问题:需要调整,同一进程,COM会优化调整。
调用地点:客户线程。
4.2 手工调整
如果通过COM方法,所有的参数都由COM库进行调整。有时候需要程序员手工对接口指针进行列集marshal和散集unmarshal,那就是在跨越套间边界,但没有通过COM库进行通信的时候。更明确的说,不通过COM接口函数,通过我们自己写的函数跨套间传递接口指针的时候。
情况一:跨套间传递接口指针。
情况二:类厂在另外的套间中,创建类实例,并传回给客户端的接口指针。
列集函数:CoMarshalInterThreadInterfaceInStream
散集函数:CoGetInterfaceAndReleaseStream
5 第三方COM组件的编写者
组件将在哪种类型的套间中执行,是编写者决定的。对于进程外组件,要调用CoInitializeEx并指定参数,以显示确定套间类型。对于进程内的服务器来说,因为客户端已经调用CoInitializeEx产生套间了,为了允许进程内的服务器可以控制它们的套间类型,COM允许每个组件有自己不同的线程模型,并记录在注册表中。
HKEY_CLASSES_ROOT/CLSID/.../InprocServer32 键值ThreadingModel
5.1 线程模型属性
组件编写者可以实现:同一个组件,既可以在STA中运行,也可以在MTA中运行,还可以在两中环境中同时存在。可以说组件有一种属性说明可以在哪种环境中生存,属性名叫做“线程模型”(相当于“隐藏”)也未尝不可。COM+里真正引入了这个属性,并叫做ThreadModel。这个属性可以有如下取值:
1. Main Thread Apartment
2. Single Thread Apartment (Apartment)
3. Free Thread Apartment (Free)
4. Any Apartment (Both)
5. Neutral Apartment (N/A)
5.2 对象在哪个套间创建
下表中第一列为套间种类,第一行为对象线程模型属性。那么,结果就是在这样的套间中创建这样的组件,组件在什么地方。在必要的时候,会创建一个代理,就是表中的宿主。
未指定
Apartment
Free
Both
Neutral
单线程
(非主)
主STA
当前套间
MTA
当前套间
NA
单线程
(主线程)
当前套间
当前套间
MTA
当前套间
NA
多线程
主STA
宿主STA
MTA
MTA
NA
Neutral
单线程
主线程套间
宿主STA
(本线程)
MTA
NA
NA
Neutral
多线程
主线程套间
宿主STA
MTA
NA
NA
5.3 属性的选择
原则是根据组件的功能选择:
如果组件做I/O,首选Free,因为可以相应其他客户端调用。
如果组件和用户交互,首选Apartment,保持消息依次调用。
COM+首选N/A。
如果没有定义,COM库默认为是Main Thread Apartment。
Apartment简单,Free强大但要自己同步。
6 鸣谢
《COM技术内幕》《COM本质论》《深入解析ATL》
6.1 MSDN2008
在MSDN 2008中相关文档的位置:
Win32和COM开发
-组件开发
-组件对象模型
-SDK文档
-COM
-COM Fundamentals
-Guide-Processes, Threads, and Apartments
Win32和COM开发
-组件开发
-COM+
-SDK文档
-COM+(组件服务)
-COM+开发浏览
-COM+ Contexts and Threading Models
6.2 COM线程模型
http://hi.baidu.com/zhangqiuxi/blog/item/ca7aa52b0311b4fbe6cd401e.html
6.3 理解 COM 套间
http://www.vckbase.com/document/viewdoc/?id=1597
6.4 泛说"COM线程模型"
http://blog.csdn.net/guogangj/archive/2007/09/06/1774280.aspx
6.5 附泛说一文
COM线程模型在COM相关的基础知识中应该算是难点,难的原因可能有这些:
1、需要对COM其它基础知识有较深的了解,因为这个论题几乎涉及到了COM所有其它的基础知识。
2、学习者得非常了解Win32本身的线程模型,因为在Windows中COM的线程模型在建立在Win32线程模型的基础上的。
3、COM线程模型所引用的概念十分抽象,不好理解。
如果你还没有掌握 1,2 所提到的知识点,你可以马上找一些书籍,迅速补充这些知识,如果你已经掌握了这些知识,那就给你的想象力上点油,轻松点。
6.5.1 开始想象
术语
公寓(Apartment)有的译文译作"套间"。这个术语抽象的是COM对象的生存空间,你还真的可以想象成公寓,线程就是住在公寓里的人。
单线程公寓(Single-Threaded Apartment STA) 这种房间是供有钱人住的单人间,设备齐全,服务周到。
多线程公寓(Multithreaded Apartment MTA) 住在这种房间里的人条件就差多了,那么多人就挤在一个大房间里头,可是他们自强不息。个个健壮得不得了。
然后思考
单线程公寓与多线程公寓的本质差别有哪些?
如果另一个人要和住在单线程公寓的人通信,不能直接去找他,哪怕你也住在高贵的单人间。但你可以打电话。提醒一下,电话每次只能同时与一个人说话(他们还没有用到电话会议之类的服务)。住在多线程公寓的人他们的房间有个大窗子,如果住在单人间(单线程公寓)的人想与他们通信,来窗口说就行,而且这个窗子比你想的要大,可以同时让很多人对话。同一房间里的人不用说了,他们可以直通话。
6.5.2 回到现实
术语
1、公寓,如果从来就不用考虑线程同步的问题,就用不着这个概念了,可是 COM 决定支持强大的多线程,于是引入了这个概念,公寓决定了线程与外界通信的方式。每一个与COM对象打交道的线程必须先决定要进入哪种公寓。
2、单线程公寓,这种公寓本身只能包含一个线程,通过调用CoInitialize(NULL)进入。它有着与窗口类似的运作方式,回想一下窗口的运行方式:消息泵不断的从消息队列提取消息,然后调度给相应的窗口函数。这样做的好处是,窗口函数永远不会重入,也就是说永远不会有同步的问题。单线程公寓也用了同样的运作方式(所以该公寓中的线程的主函数必须有一个消息循环):对该公寓中线程所拥有的COM对象的调用被队列化,只有当一个调用结束后,另个调用才会开始。那么组件对象的代码也是永远不会重入。
3、多线程公寓,这种公寓可以包含任意多的线程(具体数目由操作系统决定)。一个进程里头只能包含一个这种公寓,所有调用 CoInitializeEx(NULL, COINIT_MULTITHEADED)都会进入这个公寓。对该公寓中线程所拥有的COM对象的调用是直接的(先不考虑跨进程的情况),包括本公寓中的线程与其它的STA线程。
然后思考
单线程公寓与多线程公寓 的本质差别有哪些?
单线程公寓实现同步,有很多COM库的干预,包括将外部的调用转化成窗口消息,然后那个特别的隐藏窗口的窗口函数把窗口消息转化成COM对象的函数调用。这样的模型可以减小开发组件的难度,可是,却牺牲了效率。多线程公寓把实现同步任务全部交给了组件自己,所以在这种公寓中生存的COM对象必须足够健壮,考虑各种同步问题,不至于多个线程在调用对象的成员函数时会打架。
6.5.3 弄清它们的关系
弄清公寓,线程,对象的关系是很重要的,你弄清了吗?如果你没有弄清,那上面的这些也一定也是看得懵懵懂懂。公寓是这里面最大的单位,它是线程的容器。如果调用CoInitialize(0),COM库会创建一个STA(注意,是"创建"),你的线程将属于这个公寓,并且是这个公寓的唯一成员。如果 CoInitializeEx(NULL, COINIT_MULTITHEADED),而且是第一个要求进入MTA的线程,COM库会创建一个MTA,其它后面调用 CoInitializeEx(NULL, COINIT_MULTITHEADED)的线程会直接进入(注意,我用的"进入")已有MTA。本来线程是一个运行的实体,不会分配资源,可是在 COM的线程模型里一个对象与创建它的线程是紧密相关的,称对象归属于某个线程,至于这种归属关系是在COM库内怎么管理,我们先不去管它,以后我们把线程A创建的对象说成是线程A的对象就行了(有一个例外,得说说,有一种Single 类型的COM对象,这种对象基实就是COM在提出线程模型前的产物,这种对象总是归属于主STA线程,即第一个调用CoInitialize(0)的线程。)
在这一部分我将讲解COM提出的各个类型的线程模型,并说明COM运行时期库是如何实现它们的。 本文讲解COM提出的各个类型的线程模型,再说明COM运行时期库是如何实现它们的. 线程模型是一种数学模型,专门针对多线程编程而提供的算法,但也仅是算法,不是实现。本文讲解COM提出的各个类型的线程模型,再说明COM运行时期库是如何实现它们的,就像说明Windows是如何实现线程这个数学模型的一样,最后指明一下跨套间调用和各种类型套间编写的要求以帮助理解。希望读者对于Windows操作系统的线程这个概念相当熟悉,对何谓“线程安全的”亦非常了解。
COM线程模型
COM提供的线程模型共有三种:Single-Threaded Apartment(STA 单线程套间)、Multithreaded Apartment(MTA 多线程套间)和Neutral Apartment/Thread Neutral Apartment/Neutral Threaded Apartment(NA/TNA/NTA 中立线程套间,由COM+提供)。虽然它们的名字都含有套间这个词,这只是COM运行时期库(注意,不是COM规范,以下简称COM)使用套间技术来实现前面的三种线程模型,应注意套间和线程模型不是同一个概念。COM提供的套间共有三种,分别一一对应。而线程模型的存在就是线程规则的不同导致的,而所谓的线程规则就只有两个:代码是线程安全的或不安全的,即代码访问公共数据时会或不会发生访问冲突。由于线程模型只是个模型,概念上的,因此可以违背它,不过就不能获得COM提供的自动同步调用及兼容等好处了。
STA 一个对象只能由一个线程访问(通过对象的接口指针调用其方法),其他线程不得访问这个对象,因此对于这个对象的所有调用都是同步了的,对象的状态(也就是对象的成员变量的值)肯定是正确变化的,不会出现线程访问冲突而导致对象状态错误。其他线程要访问这个对象,必须等待,直到那个唯一的线程空闲时才能调用对象。注意:这只是要求、希望、协议,实际是否做到是由COM决定的。如上所说,这个模型很像Windows提供的窗口消息运行机制,因此这个线程模型非常适合于拥有界面的组件,像ActiveX控件、OLE文档服务器等,都应该使用STA的套间。
MTA 一个对象可以被多个线程访问,即这个对象的代码在自己的方法中实现了线程保护,保证可以正确改变自己的状态。这对于作为业务逻辑组件或干后台服务的组件非常适合。因为作为一个分布式的服务器,同一时间可能有几千条服务请求到达,如果排队进行调用,那么将是不能想像的。注意:这也只是一个要求、希望、协议而已。
NA 一个对象可以被任何线程访问,与MTA不同的是任何线程,而且当跨套间访问时(后面说明),它的调用费用(耗费的CPU时间及资源)要少得多。这准确的说都已经不能算是线程模型了,它是结合套间的具体实现而提出的要求,它和MTA不同的是COM的实现方式而已。 COM套间
Apartment被翻译成套间或是单元,是线程模型的一个实现者,就像在操作系统课程中讲到的线程只是一个数学模型,而Windows的线程、进程是它(数学模型的线程、进程)的实现者。套间只是逻辑上的一个概念,实现时只是一个结构(由COM管理)而已,记录着相关信息,如它的种类(只能是上面那三个,至少现在是),并由COM根据那个结构进行相应的处理。下面说明这三种套间的实现方式:
STA套间 一个套间如果是STA,那么那个套间有且只有一个线程和其关联,有多个对象或没有对象和其关联,就像有多个线程和一个进程关联一样,也就是说套间那个结构和某个线程及多个对象之间有关系,关系具体是什么由COM说得算,幸运的是COM正是按照上面的线程模型来定义互相之间关系的。根据上面的算法,很容易就知道只有这个线程可以访问这个套间里的对象。
COM是通过在STA套间里的线程中创建一个隐藏窗口,然后外界(这个套间外的线程)对这个对象的调用都转变成对那个隐藏窗口发送消息,然后由这个隐藏窗口的消息处理函数来实际调用组件对象的方法来实现STA的规则的。之所以使用一个隐藏窗口是为了方便组件代码的编写——只需调用DispatchMessage即可将方法调用的消息和普通的消息区分开来(通过隐藏窗口的消息处理函数)。外界对这个对象的调用都将转变成对这个隐藏窗口的消息发送来实现同步。至于COM如何截获外界对对象的调用,则是利于代理对象,后面再说明。
值得注意的是,如果使用标准汇集法生成代理对象,则代理对象会根据是进程内还是进程外的跨套间调用,来决定具体操作。如果外界线程和STA线程在同一进程内,则代理对象将直接向STA线程中的隐藏窗口发送消息;如果不在同一进程内(包括远程进程),代理对象将向RPC管理的一个线程池请求一个线程(RPC线程)来专门向另一进程中的STA线程的隐藏窗口发送消息,而不是代理对象直接发送消息,以防止外界线程由于网络等不稳定因素而导致挂起。
因为COM利用消息机制来实现STA,因此STA套间里的线程必须实现消息循环,否则COM将不能实现STA的要求。
MTA套间 这种类型的套间可以和多个线程及多个或没有对象相关联。根据上面的MTA模型,可知只有这个套间里的线程才能访问这个套间里的对象,和STA不同的只是可以多个线程同时访问对象。
外界(不属于这个套间的线程)对这个套间里的对象的调用将会导致调用线程(外界线程,也就是STA线程,因为NA没有线程)挂起,然后向RPC管理的一个线程池请求一个线程(RPC线程,并已经进入了这个MTA套间)以调用那个对象的方法。对象返回后,调用线程被唤醒,继续运行。虽然可以让STA线程直接调用对象(而不用像前述的挂起等待另一个线程来调用对象),但这是必须的,因为可能会有回调问题,比如这个MTA线程又反过来回调外界线程中的组件对象(假设客户本身也是一个组件对象,这正是连接点技术),如果异步回调将可能发生错误。
反过来,MTA的线程访问STA里的对象时,COM将把调用转换成对STA线程里那个隐藏窗口的一个消息发送,返回后再由COM转成结果返回给MTA的线程(如果使用标准汇集法生成标准代理对象,则发生的具体情况就如上面STA套间所述)。因此STA和MTA都是只能由它们关联的线程调用它们关联的对象。而根据上面所说,当MTA调STA或STA调MTA,都会发生线程切换,也就是说一个线程挂起而换成执行另一个线程。这是相当大的消耗(需要从内核模式向用户模式转换,再倒转好几回),而NA就是针对这个设计的。
NA套间 这种套间只和对象相关联,没有关联的线程,因此任何线程都可以直接访问里面的对象,不存在STA的还是MTA的。
外界(其实就是任何线程)对这个套间里面的调用都不需要挂起等待,而是进入NA套间,直接调用对象的方法。NA套间是由COM+提供的,COM+中的每个对象都有一个环境和其相绑定,环境记录了必要的信息,并监听对对象的每一次调用,以保证当将对象的接口指针成员变量进行传递或回调时其操作的正确性(保证执行线程在正确的套间内,MTA线程就是通过将自己挂起以等待STA线程的消息处理完毕来保证的),从而避免了调用线程的挂起,因此这个代理(其实也就是环境的一部分)被称作轻量级代理(相对于STA套间和MTA套间的重量级代理——需要挂起调用线程,发生线程切换)。
这个轻量级代理并不是永远都不发生线程切换。当NA对象里有个对指向一个STA对象的指针的调用而调用线程不是那个STA对象关联的线程时,调用将会转成向被调用的STA对象的关联线程发送消息,此时照样会发生线程切换。同理,如果那个对象是MTA的,而调用线程是STA线程时,依旧发生线程切换。不过除此以外的大多数情况(即不在NA对象的方法中调用另一个套间对象的方法)都不会发生线程切换,即使出现上面的情况也只有必要(MTA调NA再调MTA就不用切换)才切换线程。
根据上面所说,STA其实和MTA逻辑上是完全一样的,只是一个是关联一个线程,一个是关联多个线程而已。但把它们分开是必要的,因为线程安全就是针对是一个线程还是多个线程。而NA之所以不关联线程是因为它的目的是消除上面跨套间调用时产生的线程切换损耗,关联线程没有任何意义。
COM强行规定(不遵守也没辙,因为全是COM实现套间的,根本没有插手的余地)一个进程可以拥有多个STA的套间,但只能拥有一个MTA套间和一个NA套间,我想这应该已经很容易理解了(要两个MTA套间或NA套间干甚?)。 套间生成规则
线程在进行大多数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套间正确工作)。
套间实现规则
如前面所说,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的代理能分别正确的指出需要让哪个线程执行回调操作,即向哪个线程发送消息,因此不能混用。 |
如有缪误,敬请指正。