<<COM Threading Part 1>>
Hongjiang, 时间:
首先,COM的Apartment概念是为了让COM的开发和使用都容易才引入Apartment概念的。但是关于Apartment概念详细阐述的比较好的资料在国内可能比较少,所以有很多人对其理解上存在一些疑问。这很正常,我当初为理解它也花了2周时间,参考了不少资料。
其次,marshal的问题。marshal主要是COM用来在进程间以及计算机间进行COM调用时用的,即Proxy/stub模型。但是在进程内部有时也需要进行marshal,在下面详细阐述时,我会说明marshal相关的问题。
要注意的是,有一些COM的基本概念在这儿我不会说明,我想在这儿的讨论COM的人因该知道。还有,因为我用的是日语键盘,敲中文不方便,本文中有错别字还请多包涵。上面讲了不少废话,下面言归正传。
1。 COM Apartment的背景
大家都知道,在一个多线程的操作系统中,在线程中对一个多个线程公用的变量进行操作时,线程的同步是必须的。这个变量可以是一个简单的Integer类型,也可以是一个class或是一个COM对象。 对一个简单的Integer变量来说,线程的同步很简单,每次对它进行操作的时候用Mutex等进行同步。 对于一个class或COM对象来说,你也可以采用对简单变量一样的方法,但更好的方法是在其内部进行线程同步,这样便于使用。也就是说,你在实现这个class或COM对象时,就要写线程同步代码。如果class或COM对象内部实现了线程同步,那么它就是Thread-safe的。
现在的问题是,并不是每个人在写COM对象都保证它是Thread-safe的。 如果没有COM的Apartment, 那么我们对所有开发的COM对象都要贴上一个标签指明它是不是Thread-safe,这样使用这个COM对象的人才知道他如果要在多线程方式下使用这个COM对象时是不是要进行线程同步。 COM的Runtime为了使大家不用在自己开发的COM对象上贴上这么一个标签,而开发人员可以在没有写线程同步代码的情况下照样可以用不是Thread-safe的COM对象,引入了Apartment。
2。 COM Apartment的概念
为了解决上面所讲的Thread-safe问题,引入了COM Apartment概念。但到底COM Apartment是什么? 大家考虑一下这个问题: 如果我写了一个不是Thread-safe的COM对象,把它交给一个使用者,而且告述他是Thread-safe的。那么如果使用者在多线程环境下用我写的这个COM对象就不会写线程同步代码,会出现什么情况? 答案是明显的,执行结果会有问题。 COM的Runtime为了使使用者在开发者没有告述他COM对象的Thread-safe问题的情况下也能在各个线程模式下安全使用,要求一个COM对象能够告述COM的Runtime环境它能在什么线程模式下被安全使用, 同时,使用者在使用一个COM对象之前,也必须告述COM的Runtime环境他将在什么线程模式下使用这个COM对象,如果两者的线程模式不一样,那么COM的Runtime环境就会介入,为它们完成线程的同步问题。COM Apartment就是COM的Runtime环境对COM Client和COM对象的线程模式的包装(实际上是在TLS里面加上了线程模式的标志)。 在开发一个COM对象时开发者必须指定这个COM对象的线程模式(这一点大家应该都已经知道了)。 COM对象的线程模式会在它注册时写入系统的注册表。 使用者在每个使用COM对象的线程中必须首先调用CoInitialize,CoInitializeEx等来告述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 Client和COM对象的线程模式COM Runtime采取的动作,但对于Primary Single-Threaded Apartment,因为这种模式效率不好,基本已经不用,所以对它不进行讨论。
1)COM对象为STA,Client线程初始化为COINIT_APARTMENTTHREADED COM Runtime将建立STA COM对象,Client的线程进入这个STA并直接得到COM对象指针。
2)COM对象为STA,Client线程初始化为COINIT_MULTITHREADED 如果这个COM对象没有被建立过,COM Runtime将为这个COM对象建立一个新的线程并在这个新线程中建立STA COM对象, Client线程进入的是MTA,得到的将是新的线程中这个COM对象的被marshal的指针。如果这个COM对象被建立过,COM Client线程得到的将是已经建立的STA线程中COM对象的被marshal的指针。
3)COM对象为MTA,Client线程初始化为COINIT_APARTMENTTHREADED 如果这个COM对象没有被建立过,COM Runtime将为这个COM对象建立一个新的线程并在这个新线程中建立MTA COM对象, COM Client线程进入STA,得到的将是新的线程中这个COM对象的被marshal的指针。如果这个COM对象被建立过,Client线程得到的将是已经建立的MTA线程中COM对象的被marshal的指针。
4)COM对象为MTA,COM Client为COINIT_MULTITHREADED 如果这个COM对象没有被建立过,COM Runtime将建立MTA COM对象。COM Client线程进入MTA并直接得到COM对象的指针。如果这个COM对象被初始化过, Client线程进入已经建立的MTA并直接得到COM对象的指针。
5)COM对象为Any,COM Client为COINIT_APARTMENTTHREADED 如果这个COM对象没有被初始化过,COM Runtime将建立STA COM对象,Client线程进入STA,直接得到COM对象指针。
6)COM对象为Any,COM Client为COINIT_MULTITHREADED 如果这个COM对象没有被初始化过,COM Runtime将建立MTA COM对象,Client线程进入这个MTA并直接得到COM对象的指针。如果这个COM对象被初始化过,Client线程进入已经建立的MTA并直接得到COM对象的指针。
7)COM对象,为TNA 这个从COM+开始的线程模式比较特殊,无论COM Client用那种方式初始化,它都只存在于Client进程之外(DllHost。Exe中)。而且Client用那种方式初始化都可以'直接'进入TNA。 请注意,直接是打了引号的,其实,Client进入TNA时是通过marshal的,只是这个marshal的作用稍微有点不同,这在以后说明。
4。 关于marshal
在COM中marshal分为三种: 进程内的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+必须要截获Client对COM对象的调用才能完成将COM对象从缓冲池中取出以及放回缓冲池等的操作。
那么marshal是自动还是手工完成的呢? 方法调用是自动完成的。对象的Interface的传递,一般情况下, 是自动完成的,比如你通过调用CoCreateInstanceEx,CoGetClassObject等得到的对象Interface,以及通过方法调用传递的对象Interface。但是有些情况下必须手工marshal。 还是形象写,举个例子: 比如我有一个COM Server, 它监视一个工控装置的信号, 如果信号有异常,它要通知客户端,让客户端进行报警动作。为了实现这个功能, 客户端和我的COM Server通过IConnectionPoint完成事件触发机制。为了提高性能, COM Server的主线程接受客户端通过IConnectionPoint的Advise传来的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指针的存取,使用也很简单,这儿就不多说了,具体请参照帮助。