COM线程模型-套间

 

 

 

COM线程模型-套间

[]crybird如有转载请注明出处。

 

查找了好多资料,终于对套件这一概念有一点心得,赶紧记录下来。

首先,只要遵守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/NTANeutral 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个或1MTA

 

一个线程,进入(或创建)套间后,不能改变套间模式;但可以退出套间,然后以另外的模式再进入(或创建)另一个套间。

 

在一个进程中,主套间是第一个被初始化的。在单线程的进程里,这是唯一的套间。调用参数在套间之间被调整,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中相关文档的位置:

Win32COM开发

  -组件开发

    -组件对象模型

      -SDK文档

        -COM

          -COM Fundamentals

            -Guide-Processes, Threads, and Apartments

Win32COM开发

  -组件开发

    -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本身的线程模型,因为在WindowsCOM的线程模型在建立在Win32线程模型的基础上的。

 

3COM线程模型所引用的概念十分抽象,不好理解。

 

如果你还没有掌握 12 所提到的知识点,你可以马上找一些书籍,迅速补充这些知识,如果你已经掌握了这些知识,那就给你的想象力上点油,轻松点。

 

 

 

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)