Application Server 线程模型问题(200分)

  • 主题发起人 主题发起人 CJ
  • 开始时间 开始时间
<<<COM Threading Part 1>>>
很不好意思,前面写的不很明白.因为,在日本语键盘上敲中文实在太别扭,所以写的很短.
今天,我拼着不干活,尽量把这个问题讲清楚.还望各位多提意见.
首先,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对象.

(to be continued)
 
3. COM对象的建立与调用
在一个COM对象被建立时,COM Runtime会根据它在注册表中指定的线程模式来建立它的
Apartment.关于COM对象的各种线程模式我不想在这儿多说,到处都能找到有关的说明.
关于Apartment的模式有以下几种:
Primary Single-Threaded Apartment: 这种Apartment在一个进程中只会有一个,
而且只处在第一次建立它的线程中.它对应Single线程模型.(PrimarySTA)
Single-Threaded Apartment: 这种Apartment在一个进程中会有多个.
它对应Apartment线程模型.(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的作用稍微有点不同,这在以后说明.

(to be continued)


 
唉, 键盘敲的累死了,也没人鼓励一下.
 
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(global
interface table), 主线程将它得到的Interface指针放到GIT, 监视信号的线程从GIT
中取到的Interface指针是正确marshal了的. GIT是一个COM对象, 有三个方法提供
Interface指针的存取,使用也很简单,这儿就不多说了,具体请参照帮助.
 
如果谁有问题想和我讨论, 可以给我发Email
hongjiang@yahoo.com
or
kou@pro-jp.com
 
listen.
珍藏
 
附加功能 将问题提前
 
多人接受答案了。
 
后退
顶部