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