现在的位置: 首页 > 综合 > 正文

COM Threading Part 1

2018年04月27日 ⁄ 综合 ⁄ 共 5490字 ⁄ 字号 评论关闭

<<COM Threading Part 1>>

Hongjiang 时间: 2000-09-08

首先,COMApartment概念是为了让COM的开发和使用都容易才引入Apartment概念的。但是关于Apartment概念详细阐述的比较好的资料在国内可能比较少,所以有很多人对其理解上存在一些疑问。这很正常,我当初为理解它也花了2周时间,参考了不少资料。

其次,marshal的问题。marshal主要是COM用来在进程间以及计算机间进行COM调用时用的,即Proxy/stub模型。但是在进程内部有时也需要进行marshal,在下面详细阐述时,我会说明marshal相关的问题。

要注意的是,有一些COM的基本概念在这儿我不会说明,我想在这儿的讨论COM的人因该知道。还有,因为我用的是日语键盘,敲中文不方便,本文中有错别字还请多包涵。上面讲了不少废话,下面言归正传。

1 COM Apartment的背景

 大家都知道,在一个多线程的操作系统中,在线程中对一个多个线程公用的变量进行操作时,线程的同步是必须的。这个变量可以是一个简单的Integer类型,也可以是一个class或是一个COM对象。 对一个简单的Integer变量来说,线程的同步很简单,每次对它进行操作的时候用Mutex等进行同步。 对于一个classCOM对象来说,你也可以采用对简单变量一样的方法,但更好的方法是在其内部进行线程同步,这样便于使用。也就是说,你在实现这个classCOM对象时,就要写线程同步代码。如果classCOM对象内部实现了线程同步,那么它就是Thread-safe的。

 现在的问题是,并不是每个人在写COM对象都保证它是Thread-safe的。 如果没有COMApartment 那么我们对所有开发的COM对象都要贴上一个标签指明它是不是Thread-safe,这样使用这个COM对象的人才知道他如果要在多线程方式下使用这个COM对象时是不是要进行线程同步。 COMRuntime为了使大家不用在自己开发的COM对象上贴上这么一个标签,而开发人员可以在没有写线程同步代码的情况下照样可以用不是Thread-safeCOM对象,引入了Apartment

2 COM Apartment的概念
         为了解决上面所讲的Thread-safe问题,引入了COM Apartment概念。但到底COM Apartment是什么? 大家考虑一下这个问题: 如果我写了一个不是Thread-safeCOM对象,把它交给一个使用者,而且告述他是Thread-safe的。那么如果使用者在多线程环境下用我写的这个COM对象就不会写线程同步代码,会出现什么情况? 答案是明显的,执行结果会有问题。 COMRuntime为了使使用者在开发者没有告述他COM对象的Thread-safe问题的情况下也能在各个线程模式下安全使用,要求一个COM对象能够告述COMRuntime环境它能在什么线程模式下被安全使用, 同时,使用者在使用一个COM对象之前,也必须告述COMRuntime环境他将在什么线程模式下使用这个COM对象,如果两者的线程模式不一样,那么COMRuntime环境就会介入,为它们完成线程的同步问题。COM Apartment就是COMRuntime环境对COM ClientCOM对象的线程模式的包装(实际上是在TLS里面加上了线程模式的标志) 在开发一个COM对象时开发者必须指定这个COM对象的线程模式(这一点大家应该都已经知道了) COM对象的线程模式会在它注册时写入系统的注册表。 使用者在每个使用COM对象的线程中必须首先调用CoInitializeCoInitializeEx等来告述COM Runtime环境将在那种线程模式下使用COM对象。

3 COM对象的建立与调用

 在一个COM对象被建立时,COM Runtime会根据它在注册表中指定的线程模式来建立它的Apartment。关于COM对象的各种线程模式我不想在这儿多说,到处都能找到有关的说明。

关于Apartment的模式有以下几种:

Primary Single-Threaded Apartment: 这种Apartment在一个进程中只会有一个, 而且只处在第一次建立它的线程中。它对应Single线程模型。

(PrimarySTA)Single-Threaded Apartment: 这种Apartment在一个进程中会有多个。它对应partment线程模型。(STA)

Multi-Threaded Apartment: 这种Apartment在一个进程中只会有一个,  它对应Free线程模型。

(MTA)Any : 这种Apartment在一个进程中可能有多个,也可能只有一个, 它对应Both线程模型。(STA/MTA)

Thread-Neutral Apartment: 这时在COM+中出现的, 它一直执行在COM Client的进程之外的独立的进程之中。对应Neutral线程模型。(TNA)

COM Client在指定它所使用的线程模型时在调用CoInitializeEx是第二个参数用Single-Threaded Apartment(COINIT_APARTMENTTHREADED)Multi-Threaded Apartment(COINIT_MULTITHREADED)来指明进入的是那种Apartment

COM Runtime在建立COM对象时根据它在注册表中指定的线程模式将COM对象建立相应的Apartment之中, 然后根据COM Client所使用的线程模式, COM调用采取不同的动作。下面我想讨论一下不同COM ClientCOM对象的线程模式COM Runtime采取的动作,但对于Primary Single-Threaded Apartment,因为这种模式效率不好,基本已经不用,所以对它不进行讨论。

1)COM对象为STAClient线程初始化为COINIT_APARTMENTTHREADED COM Runtime将建立STA COM对象,Client的线程进入这个STA并直接得到COM对象指针。

2)COM对象为STAClient线程初始化为COINIT_MULTITHREADED 如果这个COM对象没有被建立过,COM Runtime将为这个COM对象建立一个新的线程并在这个新线程中建立STA COM对象, Client线程进入的是MTA,得到的将是新的线程中这个COM对象的被marshal的指针。如果这个COM对象被建立过,COM Client线程得到的将是已经建立的STA线程中COM对象的被marshal的指针。

3)COM对象为MTAClient线程初始化为COINIT_APARTMENTTHREADED 如果这个COM对象没有被建立过,COM Runtime将为这个COM对象建立一个新的线程并在这个新线程中建立MTA COM对象, COM Client线程进入STA,得到的将是新的线程中这个COM对象的被marshal的指针。如果这个COM对象被建立过,Client线程得到的将是已经建立的MTA线程中COM对象的被marshal的指针。

4)COM对象为MTACOM ClientCOINIT_MULTITHREADED 如果这个COM对象没有被建立过,COM Runtime将建立MTA COM对象。COM Client线程进入MTA并直接得到COM对象的指针。如果这个COM对象被初始化过, Client线程进入已经建立的MTA并直接得到COM对象的指针。

5)COM对象为AnyCOM ClientCOINIT_APARTMENTTHREADED 如果这个COM对象没有被初始化过,COM Runtime将建立STA COM对象,Client线程进入STA,直接得到COM对象指针。

6)COM对象为AnyCOM ClientCOINIT_MULTITHREADED 如果这个COM对象没有被初始化过,COM Runtime将建立MTA COM对象,Client线程进入这个MTA并直接得到COM对象的指针。如果这个COM对象被初始化过,Client线程进入已经建立的MTA并直接得到COM对象的指针。

7)COM对象,为TNA 这个从COM+开始的线程模式比较特殊,无论COM Client用那种方式初始化,它都只存在于Client进程之外(DllHostExe)。而且Client用那种方式初始化都可以'直接'进入TNA 请注意,直接是打了引号的,其实,Client进入TNA时是通过marshal的,只是这个marshal的作用稍微有点不同,这在以后说明。

4 关于marshal

 COMmarshal分为三种: 进程内的marshal 同一计算机中进程间的marshal,以及不同计算机间的marshal 进程内和进程间的marshal是通过Local RPC完成的,计算机间的marshal通过DCE RPC来完成。 进程间和计算机间的marshal是必须的,进程内marshal是在不同Apartment之间进行方法调用和传递对象Interface时发生。

在同一Apartment内的调用用不着marshal 举个例子来说, 一个处于MTA中的Client线程,想要调用一个处于STA中的对象时, COM Runtime会走进来, 对这个调用进行marshal。为什么要marshal? 因为MTA本身说明了现在是一个多线程的环境, STA中的对象不是Thread-safe的,那么对这个不是Thread-safe的对象的调用必须要序列化(排队)

COM Runtime为了保证不是Thread-safe的对象的调用序列化, 必须要截获对该对象的调用, 然后进行排队。 marshal就是起这个作用。 实际上,COM Runtime会截获MTA中的线程对STA对象的调用(通过Proxy),将这个调用通过消息传递方式传递给STA对象的stub 在完成调用后由stub将结果传回Proxy 对于对象的Interface 也是同样的道理。

 对于COM+Thread-Neutral Apartment比较特殊, 它是一直需要marshal的, 一个方面是因为它处于不同的进程中。另外一个重要原因是, COM+提供了一系列新的功能, Object Pooling Object Construct String等。 COM+必须要截获ClientCOM对象的调用才能完成将COM对象从缓冲池中取出以及放回缓冲池等的操作。

 那么marshal是自动还是手工完成的呢? 方法调用是自动完成的。对象的Interface的传递,一般情况下,  是自动完成的,比如你通过调用CoCreateInstanceExCoGetClassObject等得到的对象Interface,以及通过方法调用传递的对象Interface。但是有些情况下必须手工marshal 还是形象写,举个例子: 比如我有一个COM Server 它监视一个工控装置的信号, 如果信号有异常,它要通知客户端,让客户端进行报警动作。为了实现这个功能, 客户端和我的COM Server通过IConnectionPoint完成事件触发机制。为了提高性能, COM Server的主线程接受客户端通过IConnectionPointAdvise传来的Interface指针, 并将之放到一个Interface指针表里, 主线程运行在STA中。 另外建立了一个运行于MTA中的线程专门用来监视信号,如果信号异常,它将调用Interface指针表里所有Interface的方法来通知客户端。 现在如果理解COM的机制的人看到我这个实现方法就知道这里面需要对Interface指针进行手工marshal 为什么, 客户端通过IConnectionPoint传给我的COM Server主线程的Interface指针是自动进行了marshal 但是, 由于我的COM Server的专门用来监视信号的线程运行在和主线程不同的Apartment之中, 对这个线程来说, 这些Interface指针是没有经过marshal的, 在调用是就会出现RPC_E_WRONG_THREAD错误。 要解决这个问题,有两个办法,

 1) 让我的主线程也运行在MTA中。这种方法简单。 2) 手工marshal 在主线程中得到客户端的Interface指针后, 调用CoMarshalInterThreadInterfaceInStream 得到一个IStream的指针,让后将它放到IStream的指针表里, 监视信号的线程要通知客户端时,从IStream的指针表取得IStream指针, 然后调用CoGetInterfaceAndReleaseStream得到marshal后的客户端Interface指针。 这种方法有个缺点, 就是一旦调用CoGetInterfaceAndReleaseStream后这个IStream指针就被释放掉了,下一次就取不到了。 更好的解决方法是采用GIT(globalinterface table) 主线程将它得到的Interface指针放到GIT 监视信号的线程从GIT中取到的Interface指针是正确marshal了的。 GIT是一个COM对象, 有三个方法提供
Interface
指针的存取,使用也很简单,这儿就不多说了,具体请参照帮助。

 

 

抱歉!评论已关闭.