COM与套间(Apartment)之我见

COM与套间之我见

                                                               作者:ynb  2009.12.08

 

前言

   这篇文章,只写给那些对COM有一定了解,并想搞清楚所谓COM多线程特性的有志青年,以求共勉。

   潘爱民的《COM原理与应用》里,在描述COM的多线程特性时,有这么一句话:“虽然两个线程共用同一个进程地址空间,但他们使用不同的堆栈,因此,跨线程调用如同跨进程调用一样,也要通过代理/存根模块进行间接调用。”。

就是这句话,又让刚刚对套间觉得有一知半解的我陷入了迷魂阵。

堆栈怎么跟接口调用撤上关系呢,堆栈保存的是数据,也就是个自定义变量什么的,接口指针可是代码段的指针啊。

 

    COM接口在线程之间传递一定要用代理/存根么?

  如果是,为什么呢?

  如果不是,又是什么情况下可以直接传递指针呢?

 

带着重重疑问,我抛开书本,打开百度,以求集思广益。功夫不负有心人,剥开层层迷雾,终于让我悟出了其中奥妙,同时也看出了网络上很多文章的缺陷。

 

那么《COM原理与应用》的那句话是否正确呢,看过正文,你就会知道作者写那句话时是有些不负责任的。

 

   下面,我将说收我的理解,有不同观点的,非常乐意大家来说服我。

我这人很倔的。^_^

 

 

 

 

1.   几个概念

首先先来搞明白几个概念。比较长,要有足够的耐心

  · 应用程序

  · 进程

  · 线程

 

1.1  应用程序

应用程序是完成某种功能的一系列程序的集合。

一个应用程序可以有一个或多个进程构成

 

Ø   一个进程的情况

  可以简单的理解为一个可执行的EXE程序。当然是程序导入内存之后才算进程,要不然文件就是文件。

 

Ø   多个进程的情况

  既然说的是COM,就拿COM举个例子

  使用进程外组件的应用程序,就是由多个进程构成的。

  什么是进程外组件?我前言里说了,是写给对COM有一定了解的。。。。

 

1.2  进程

1.2.1     进程的描述

进程在计算机里边,就是一个单独的运行模块,有单独的地址空间,代码,以及资源。

进程有一个或多个线程组成,但至少要有一个线程,就是进程本身,称为主线程。其他根据需要新建的线程,称为辅线程。

这里对地址空间问题简单描述一下,因为后面用到。

 

1.2.2     地址空间

相信对操作系统有所了解的,都知道虚拟地址,物理地址之说。

Ø   物理地址:

  真是的物理内存地址,是有限的,一般个人装机器,配个4G内存就觉得很 NB了。(32位总线最多支持4G)

Ø   虚拟地址:

  对物理内存地址所做的映射,也可以理解为映射地址。不管你的物理内存有多大,256M也好,4G也好,地址总是0-4G。

 

进程用的是虚拟地址,也就是说,不同的进程,其头地址都是0,地址空间都是4G

 

那具体这些地址是指向物理内存的什么地方呢,这个由系统来管理,进程启动时,由系统在物理内存分配一个地方,把进程的东东导入到内存。并记住虚拟内存和物理内存的映射关系。你用的时候,他会自动给你转换成物理地址,给你取出你需要的数据。很有可能,这个地址指向硬盘呢。为什么?因为程序太大,物理内存不够用啊,数据还在硬盘存着呢,你要用时系统再把他读到内存。(当然,得把占用内存的一部分数据,写到硬盘存起来,好腾地方)。这个不是重点,很有兴趣地还是请搜索互联网。

 

总之,我们不必关心数据具体存在物理内存的什么地方,咱们只要知道所需要数据的虚拟地址就可够了,其他的交给操作系统。

 

 

  

1.2.3     内存模型

 

                        

 

 

 

1.2.4     总结

由于上述原因,我们完成两个进程之间的共享数据时,只能使用内存映射等特殊的方法。因为直接传数据地址不管用嘛,你说的1号地址跟我的1号地址,那简直驴唇不对马嘴。

顺便提一下,线程外组件要用代理也是这种原因。当然它更复杂一些,还要处理远程调用。

 

1.3  线程

1.3.1     线程的描述

线程就是进程的小弟了。进程就是黑社会老大,招几个小弟(创建几个线程),给每个小第(线程)分配一个去收保护费的任务,这些小弟(线程)呢,就分头去收钱(同时进行),收回来报告给老大。

这里可以限制完成任务的时间,也可以不限制时间。不限制时间的话,老大就等到收回来为止,但这种老大不多见了,大多数老大,都采用限制时间的做法,给你两天,收不回来就枪毙你。进程也一样,给你一定的时间去执行,一定时间内还没执行完,就杀掉线程,因为不能无限制的等啊,你死循环了怎么办?

 

1.3.2     线程的内存模型

线程使用程的源,也使用程的地址空,但是有自己的堆

 

 

 

堆栈是干什么用的呢。(仅限我所知道的)

  Ø   保存自定义变量

  Ø   保存函数返回地址

  Ø   保存函数参数

  Ø   保存寄存器的值

  Ø   肯定不能把代码保存进来

 

 

 

  这里有一点非常重要:线程使用进程的地址空间。也就是说,虽然每个线程有自己的堆栈,但他还是同一个进程内的地址空间,用图的话,可以这样表示:

 

                

 

 

1.3.3     总结

这里我们总结出很重要的一点:

l  只要在同一个程内,地址是可以在线程之任意传递并且是有效的可操作地址。

 

2.   套间的由来

2.1  对与错

再回到最开始引用《COM原理与应用》里的那句话:“虽然两个线程共用同一个进程地址空间,但他们使用不同的堆栈,因此,跨线程调用如同跨进成调用一样,也要通过代理/存根模块进行间接调用。”

这句话是解释不通的,不信的话,大家可以试一下,把主线程的自定义变量的地址传给另外一个辅线程,然后在辅线程内操作它,也有可能成功。为什么说也有可能,因为牵扯的多线程调用的并发竞争问题。成功的前提是你不要在主线程也对他做一些可能引起竞争的读写操作。我说得不成功不是出错,而是得不到你所需要的结果。

COM也如此,跨线程传递接口地址,也有可能成功。如果让两个线程用同一个接口,调用完全不相干的两个函数,肯定能成功。不成功是因为,COM内部没有多线程控制,导致了内部竞争。这个可就真会出错了,COM太复杂,限于本人能力有限,只能说到这里了。

 

2.2  多线程控制

线程是同步运行的,虽然是同步,但也只是表面,CPU毕竟只有一个(注1),怎么可能让两个程序同时运行。操作系统只不过是给每个线程分配一个时间片,让其执行一段时间,然后让其睡眠,腾出CPU,让给另外的线程。也就是说,操作系统会在任意时刻唤醒一个线程,而让另一个线程睡眠。多线程调用很能会导致你想象不到的各种并发控制问题。就连简单的 a++ 都要小心。

(注:)现在大多数CPU都是双核,4核之类的了,咱们只讨论一个核的情况。多核也要处理好单核的问题嘛。

 

2.3  一个例子

虽然a++是一句话,但编译成汇编语言执行时是好几句话:

  Move 寄存器,变量地址  ―――把内存数据读到寄存器

  Add  寄存器,1     ―――数据+1

  Move 变量地址,寄存器  ―――把数据移回内存 

 

    

 

 

 

两个线程顺序执行时,结果大家应该很清楚,应该是a=3

也就是说,多线程状态下,如果你不加控制,a的结果是不确定的,有可能是2有可能是3。这肯定是不允许的,然而多线程状态下到处是这种情况。

 

2.4  套间的提出

   COM也不例外,多线程下操作COM如何解决并发控制的问题呢?如果让大家自己解决,相信会浪费大家不少宝贵的时间,为了给开发者节省时间,微软提出了套间(apartment)。

 

 

                进入正题

3.   是套(apartment)

    看了好多资料,都觉得稀里糊涂,我觉得还是英文直译最好,翻翻英文字典,就知道他他既不是进程也不是线程,是一个屋子、一个房间的意思。

    也就是微软为我们设计了一个屋子,只要在这个屋子里调用COM,那么就没有并发控制的问题。这个屋子提供了一些内部机制为我们解决这些棘手的并发控制问题。

 

    屋子什么时候创建的呢?是在调用CoInitialize,或者CoInitializeEX时创建的。

    这也就是我们编写COM应用程序,要调用这两个函数的原因。

    那么套间是如何解决这些问题的呢?

 

4.   的内部机制

  我们都应该知道Windows系统的消息机制,每个窗口应用程序,都会有一个消息循环,他会一直等待一个消息来启动相应的执行程序。同一时刻会有很多消息都发过来,系统把这些消息的组成了一个队列,窗口通过消息循环,一个一个取出来去处理他,在处理完前一个消息之前,不会取下一个消息,这样就会让程序顺序执行,不会有并行的问题了

 

 

 

         

 

 

5.   与安全性

5.1  套间的种类

套间又分几种,它还根据你所声明的COM的多线程安全性提供不同的服务。

Ø   STASingle-Thread Apartment  单线程套间

Ø   MTAMulti-Thread Apartment   多线程套间

 

5.2  COM的多线程安全性

Ø  Single     多线程不安全,一个进程中只能创建一个对象,只能在STA中使用

Ø  Apartment 多线程不安全,一个进程中可以创建N个对象,只能在STA中使用

Ø  Free         多线程安全,一个进程中只能创建一个对象,只能在MTA中使用

Ø  Both    多线程安全,既可以在STA中使用,也可以在MTA中使用

 

5.3  注意点

这里千万不要混淆二者的概念。套间是为COM提供服务的,为了提供恰到好处的服务,针对不同的类型的COM应该创建不同的套间。

 

6.   套间的工作

下面我们看看套间对不同安全类型的COM都提供了哪些服务。

 

6.1  SingleCOM

如果一个COMSingle型的,首先说明它是多线程不安全的。也就是多线程同时访问会出现问题。

还是根据微软的解决方案,我们创建一个单线程套间(STA)来给这个COM提供服务。

 

这里STA都干些什么呢?(当然是针对同一个COM

Ø   它会在你看不见的地方,为COM创建一个窗口,等待发给COM的消息,根据不同的消息,再调用COM不同的接口。

Ø   在第一次创建COM的线程中,在取得COM接口指针时,为了避免自己给自己发消息,损失效率,直接返回接口指针,以供调用。

Ø   在其以后创建相同COM的线程中,实际上不再重新创建COM,只是返回已经创建好的COM的代理接口指针,代理接口完成向窗口发送消息的功能。

 

其实还有一种大家熟悉的方法可以完成STA相同的功能,那就是:

Ø   CoMarshalInterThreadInterfaceInStream 函数

Ø   CoGetInterfaceAndReleaseStream 函数

 

CoMarshalInterThreadInterfaceInStream的功能就是把代理打包(COM里叫做列集)传给另一个线程

CoGetInterfaceAndReleaseStream功能就是从打包的代理中解析出(COM里叫做散集)代理接口指针

 

总之,对待Single型的COM,有两种方法完成其多线程控制

·    使用单线程套间(STA)

·    使用上便提到的列集和散集函数

 

注意一点:第一次创建COMSTA返回的是COM真实的接口指针而不是代理接口指针。

 

经常出现的疑问:

²  ?你想把个接口直接传递给其他线程?

    人家都声明是多线程不安全类型的COM的了,你还夸线程直接调用?人家都告诉你禁止转载了,你还到处copy?那么运气好一点,天下太平,运气不好那你就要被告上法庭了。

 

²  我如果我创建一个MTA来管理它会出现什么结果?

    这个问题问得好!

    结果就是: MTA再创建一个新的线程,在线程里做一个STA后,在STA里再创建一个COM对象,然后把代理指针借口传给原来的线程。

 

 

    比如原来进程有 :

·   线1  创建COM的线程

·   辅线程2  意欲调用COM接口的线程

果就是:

·   主线程1  创建一个线程,这里成为COM线程

·   COM线程  创建一个STA,新建一个COM对象,把代理接口传给主线程1

·   主线程1  获得代理接口,传给辅线程2,自己也可以调用

·   辅线程2  调用代理接口

 

很明这样使得新建的COM线程就成了管理COM的主线程(虽然不是我们创建的),我们的线程就成了辅线程,都要通过代理(发送消息)来调用COM的接口。

这样一来,肯定是变成多线程安全的COM了,因为我们第一步得到的就是一个代理接口地址,你可以在线程之间任意传递,不会有任何问题。但对于本来可以直接调用的接口,都要通过发消息来实现,可见效率损失是很大的。千万不要这样做,如果运气好,CoInitializeEX可能会给你返回一个错误(RPC_E_CHANGED_MODE)来提示你。我并未测试,只是看MSDN说会返回这样一个错误。

重中之重:STA是通过消息机制实现的,STA创建了窗口,以及消息处理函数,但并未创建接受消息的函数,所以如果要用STA解决多线程调用COM问题,一定在主线程(第一个创建COM)的线程中,消息循环来截获消息。当然,如果你只有一个线程,那就不用了。。。

 

6.2  ApartmentCOM

    如果一个COMApartment型的,也说明它是多线程不安全的。也就是多线程同时访问会出现问题。(注意:这里的Apartment型跟套间是两个概念,不要混淆)

 

    根据微软的解决方案,我们还是要创建一个单线程套间(STA)来给这个COM提供服务。

    但这跟Single有什么相同点和不同点呢?

 

n  相同点:

Ø  Single类型的COM一样,STA仍然会为会创建一个窗口,等待消息

Ø  在第一次创建COM的线程中,STA仍然直接返回COM的接口指针

 

n  不同点:

Ø  在其以后建相同COM的线程中,并非返回已有COM的代理接口指针,而是实际创建一个新的COM对象,返回其真实的接口指针。

  

也就是每个线程中都有一个COM对象,他们彼此互不相干,当然不会有什么并发控制问题。

想让不同线程中,访问同一个Apartment型的COM

简单,还是用CoMarshalInterThreadInterfaceInStream函数。别忘了加上消息循环。

 

²  想把个接口直接传递给其他线程?

        你又想上法庭了?

 

²  我如果我建一个MTA来管理它会出现什么结果?

    这个跟Single的处理结果一样。自己动动脑子啊。。

 

5.3  FreeCOM

如果一个COMFree型的,说明它是多线程安全的。

根据微软的解决方案,这里需要创建一个多线程套间MTA

 

多线程套间(MTA)都干些什么呢?

其实什么都没干,既然你是多线程安全的,要我不是多余的么?

线程套间(MTA)可能在导入COM类库等方面做些文章,但对于多线程控制,是什么都不做的。

 

²  下好了。既然它是多线程安全的,我是不是可以线程之直接传递接口指

    正解。你可以把接口指针传给进程内部的其它任何线程,然后直接调用它。

 

注意:并不是所有的COM都可以简单的声明成Free型的。

声明成Free型的COM需要付出一定的代价。那就是你要把COM编写成多线程安全的COM。也就是要控制好一大堆多线程并发控制问题。

 

²  我如果我建一个STA来管理它会出现什么结果?

    很多文章说,会创建一个跟COM匹配的套间来管理它。

    我并未深究,讨论这个问题还什么意义么?

    多线程都安全的了,你管你创建啥套间呢,反正你你第一次给我传过来的是真实的接口地址,我就可以任意传,为所欲为。。。如果你给我返回(RPC_E_CHANGED_MODE)之类的错误,不让我创建STA套间,那我就没办法了。

 

 

6.4  BothCOM

Both型的COM必须也是一个多线程安全的。

但是跟Free不一样的是,它明确声明既可以创建一个单线程套间(STA),也可以创建一个多线程套间(MTA)来管理它。

 

²  为什么要这样做?

据个例子COM A内部调用了COM BCOM A我要把它做成一个Free型的,而COM B是个已经做好的Single型的。

 

·   如果我们创建一个MTA来管理COM A

    COM AFree型的,会直接返回接口指针。但COM B是一个Single型的,根据Single类型里的讲述,MTA会创建另外一个线程来管理COM B,然后把代理接口指针传回来。虽然有些效率损失,但你声明是个MTA,为了线程安全,也只能这么做。

 

·   如果我们创建一个STA来管理COM A

    COM A是多线程安全的,自然直接返回接口指针。而COM B也是正好需要一个STA来管理它,那STA会直接返回COM B接口的指针。

 

  字太多了点,图太少了点,也没办法,工作忙,没有太多时间来整理。

  看得懂的看门道,看不懂得看热闹吧。。希望大家支持。

  谢绝转载。谢谢合作!

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值