COM组件以及套间

COM组件的特点:
1. COM组件是以WIN32动态链接库(DLL)或可执行文件(EXE)形式发布的可执行代码组成。
2. COM组件是遵循COM规范编写的   COM组件是一些小的二进制可执行文件
3. COM组件可以给应用程序、操作系统以及其他组件提供服务
4. 自定义的COM组件可以在运行时刻同其他组件连接起来构成某个应用程序
5. COM组件可以动态的插入或卸出应用
6. COM组件必须是动态链接的
7. COM组件必须隐藏(封装)其内部实现细节
8. COM组件必须将其实现的语言隐藏
9. COM组件必须以二进制的形式发布
10.COM组件必须可以在不妨碍已有用户的情况下被升级
11.COM组件可以透明的在网络上被重新分配位置
12.COM组件按照一种标准的方式来宣布它们的存在
套间:
套间的提出是为了组件在多线程环境下安全执行,因为有跨线程调用同一个组件方法的状况存在。若该组件接口是线程安全的,则无须套间,否则需要套间的协助,就如窗口过程函数一样,
窗口过程本身并不是线程安全的,但是消息队列的机制,保证了窗口过程总是在一个线程中执行,串行地处理消息。

要理解COM 和线程,必须要清楚套间(apartment)
套间只是一个应用程序中的逻辑容器,在这个套间之内,套间内所有COM对象遵守套间内线程规则,
比如 从一个套间内或者套间外线程访问对象的方法或者属性都需要遵守一样的规则

套间的目的是什么呢?
在任何包括多线程的程序中,都有可能存在多个线程同时访问COM对象的问题,怎样解决这种冲突呢?
那就是套间,COM套间出现的主要目的就是为了线程安全,主要是指对象内属相和方法,全局属相和方法或者静态属相和方法除外,需要COM组件代码进行保护。


套间包括下面节点规则:
1.每个COM对象只能属于一个套间,属于哪个套间,是在COM对象创建的时候就决定了。
2.一个COM线程(创建或者使用COM对象的线程)也只能属于一个套间,跟COM对象一样,一旦确定就不能改变
3.线程和属于同一个套间的COM对象会遵守相同的调用规则,比如相同套间内的线程调用对象的方法,就会直接执行,与COM特性就没有任何关系。
4.线程和COM组件如果是不同的套间,那他们各自的规则不一样,方法的调用就会经过Marshal, 那就需要proxies和stubs.

除了保证线程安全,另外一个好处就是COM对象本身或者客户端不需要关心对方的套间模型。

指定COM组件的套间模型
在DLL类型的COM 服务。 其中的coclass会指定线程模型,并且在注册之后再注册表里就会有ThreadingModel键值,这个键值在InprocServer32下,如果用ATL编程,在加组件的时候,VS会有选项去设置。
COM组件的线程模型(Threading Model)共有四种:Single Apartment Both Free(可以通过修改注册表直接改变这个组件的Treading Model)
Single: 当一个组件的线程模型被标识为Single,说明进程中这个组件的实例 都必须在同一个套间线程中。(该套间线程就是进程创建的第一个STA套间线程)。无论在进程中创建多少个组件实例,接口调用都是在同一个线程中完成的。这 其中当然涉及到给隐藏窗口发送消息和等待,需要注意的是避免
线程间的死锁问题。
Apartment: 当一个组件的线程模型被标识为Apartment,当创建了一个组件,并把组件的某个接口传递给另一个线程时,在这个两个线程中调用这个接口提供的方法,最终都是在同一个线程中完成的。说明该组件不是线程安全的,需要套间的协助。所处的套间线程必须是
COINIT_APARTMENTTHREADED。
Free: 说明组件是线程安全的,当发生2.2的状况时,调用是在不同线程中完成的。但是所处套间必须是MTA(COINIT_MULTITHREADED)
Both: 说明组件是线程安全的,所处的套间MTA(COINIT_MULTITHREADED)和STA(COINIT_APARTMENTTHREADED)都可以。


指定线程的套间模型
一个COM线程(创建COM对象或者要使用COM对象的线程)必须进行初始化,初始化函数有CoInitializeEx()和CoInitialize(),CoInitializeEx是一个创建套间的过程,我们使用CoInitializeEx(NULL, COINIT_MULTITHREADED)后,会创建一个MTA套间。CoInitializeEx(NULL,
COINIT_APARTMENTTHREADED)创建个STA套间。一个进程可以包含多个STA,但只能有一个MTA。 一个STA只能包含一个线程,一个MTA可以包含多个线程。
缺省套间线程
当在创建组件时所处的线程(所创建的套间)与该组件的线程模型不匹配。这种状况下系统就会把组件对象放入缺省的套间中(当然是运行在一个缺省的线程中).
套间的本质
套间是保存在线程的TLS中的一个数据结构,借用该结构使套间和线程之间建立起某种关系,通过该结构可以帮助不同的套间之间通过消息机制来实现函数的调用,以保证多线程环境下,数据的同步。

STA

Figure 1


一个STA只能包含一个线程,但是可以包含很多的COM对象。一个套间至少包含一个线程。特别的是,如果有对象要给其他线程用,那么这个线程必须有消息循环。

STA的访问规则
1.STA线程中创建的所有STA对象都会和线程在一个套间内
2.所有STA对象的调用都是通过这个线程来实现的

Figure 2

通过上面图片表明,两个不同套间的线程在同一个DLL中创建对象,也属于不同的套间
对于第二点,有两个方式去调用对象的方法:
1. 在相同STA的线程内调用,调用方法直接顺序调用
2. 在另外一个套间内的线程调用,这个时候COM要保证通过套间线程内调用,那就必须有消息循环。

消息循环

Figure 3


一个包含消息循环的线程,大家都知道的就是UI线程, 一个包含一个或者多个窗口的UI线程,通常会表示说这个线程包含了这么多的窗口,窗口的调用都是通过拥有这个窗口的线程来实现的,这就是这个消息循环中的API DispatchMessage().
一个线程可以通过post/send message到这个从窗口,然后目标窗口的线程就会GetMessage(),然后去执行DispatchMessage(),所以对这个窗口的任何调用都是同步的,顺序执行的。线程的消息循环就保证了这种同步,这样开发人员就不需要去处理同步的问题或者多线程调用的问题,因为只
有一个消息处理完了,才会处理下一个消息。

同样COM内嵌的窗口也同样可以实现线程安全,所有从套间外调用COM对象的方法都是通过COM发送消息到隐藏的窗口来完成的,隐藏的窗口就会把收到的消息进行排序一个一个处理,然后返回调用结果。

当涉及到套间外调用方法,COM就会准备proxies和stubs,从这点看消息循环只是STA的一个协议。

有两点要注意:
1. 上面提到的通过消息循环处理STA COM对象的调用,只是当套间外调用的时候,如果是套间内调用,就不需要走COM
2. 如果STA中的消息循环失败,不能get和dispatch消息,那么套间线程就不会再收到其他套间发来的消息。
考虑到第2点,有些API,比如Sleep, WaitForSingleObject, WaitForMultipleObjects将会破坏消息循环的流程,

STA的好处:
用STA的第一个好处就是简单,在COM类型内除了基本的代码,很少或者根本不需要同步代码,COM会自动同步,顺序执行操作,这一点对ActiveX特别有用。
因为STA对象由线程的亲缘性(所有操作都是同一线程来执行的),那么STA对象的开发人员,可以在这个线程内跟踪对象的数据。

STA的坏处:
STA架构会造成performance的问题,如果有多个线程去访问对象的方法,那么由于STA只有一个消息循环,那么就会多个线程同时等待,顺序执行这些调用。同样要是STA包含多个COM对象,也会导致

实现COM对象和服务

STA COM可以使开发人员忽略并发修改COM对象内部数据,但是全局数据的修改或者全局函数的调用STA COM就不会保护,比如全局函数DllGetClassObject等,主要是因为STA对象可以在不同的线程内创建。创建的套间不同,可以同时访问全局变量或者函数方法。
大家都知道的COM对象个数的全局变量,这个变量在DllGetClassObject和DllCanUnloadNow中修改,所以在这两个函数中,利用了InterlockedIncrement和InterlockedDecrement来实现同步,
因此在实现COM服务的时候要注意:
1. DLL 服务器必须包含线程安全标准函数 DllGetClassObject()和DllCanUnloadNow(),
2. 私有的全局函数必须保证线程安全
3. 私有的全局变量必须保证线程安全

默认STA
int main()
{
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2;
    /* If a default STA is to be created and used, it will be created */
    /* right after spISimpleCOMObject2 (an STA object) is created. */
    spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

在这个main函数中,我们分析一下:
1. 在main函数中调用了::CoInitializeEx(NULL, COINIT_MULTITHREADED);, 表明该线程是MTA线程
2. 调用DisplayCurrentThreadId()去显示当前线程的ID。
3. 创建STA COM对象spISimpleCOMObject2。
4. 注意到spISimpleCOMObject2是STA对象,而当前线程不是STA线程,这时候COM就会创建一个default STA。
5. 调用TestMethod1()(该函数打印调用的线程),就会发现线程的ID不是main线程的ID。
所有的STA COM对象如果在非STA里创建都会属于这个default STA 里面,与创建这些STA对象的线程不同套间

Figure 4


通过图显示,spISimpleCOMObject2->TestMethod1()是跨套间的调用, 这需要marshal, 因此main创建的COM对象返回的实际上是proxy,不是真正的指针。

下面一个例子可以证明,非STA创建的STACOM对象实际上实在default STA的。
int main()
{
  HANDLE hThread = NULL;
  DWORD  dwThreadId = 0;
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2;
    spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2 -> TestMethod1();
    hThread = CreateThread
    (
      (LPSECURITY_ATTRIBUTES)NULL, // SD
      (SIZE_T)0,      // initial stack size
      (LPTHREAD_START_ROUTINE)ThreadFunc, // thread function
      (LPVOID)NULL,                       // thread argument
      (DWORD)0,                    // creation option
      (LPDWORD)&dwThreadId         // thread identifier
    );
    WaitForSingleObject(hThread, INFINITE);
    spISimpleCOMObject2 -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

DWORD WINAPI ThreadFunc(LPVOID lpvParameter)
{
  ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
  DisplayCurrentThreadId();
  if (1)
  {
    ISimpleCOMObject2Ptr spISimpleCOMObject2A;
    ISimpleCOMObject2Ptr spISimpleCOMObject2B;
    spISimpleCOMObject2A.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2B.CreateInstance(__uuidof(SimpleCOMObject2));
    spISimpleCOMObject2A -> TestMethod1();
    spISimpleCOMObject2B -> TestMethod1();
  }
  ::CoUninitialize();
  return 0;
}

具体分析一下:
1. 在main 函数中调用 CoInitializeEx(NULL, COINIT_MULTITHREADED) ,表明该线程属于MTA
2. 调用DisplayCurrentThreadId显示main线程的id, 比如thread_id_1
3. 创建STA COM对象 spISimpleCOMObject2
4. 调用TestMethod1,这时候显示的调用线程为thread_id_2,不是主线程。
5. 启动一个新的线程执行函数ThreadFunc(),在函数里面初始化该线程属于MTA
6. ThreadFunc调用DisplayCurrentThreadId显示该线程的id,假设为thread_id_3.
7. 创建两个STA对象spISimpleCOMObject2A和spISimpleCOMObject2B
8. 对两个对象调用TestMethod1(), 发现输出的ID是thread_id_2.
该例看出来spISimpleCOMObject2, spISimpleCOMObject2A和 spISimpleCOMObject2B都属于default STA 

Legacy STA (ThreadingModel 是Single)
Legacy STA 与标准的STA有一点不同,就是所有的Legacy STA COM 对象都属于一个Legacy STA, 一个线程中。
 

Figure 5

可以看出非STA线程创建的legacy STA对象获得的实际上是被Marshal过的,创建线程返回的是Proxy的对象,不是实际的指针,这点跟标准STA一样。


STA之间COM对象的互相调用
确切的说只要是套间之间的COM对象的互相调用,都要对COM对象进行列集(Marshal),如果不列集,散集,那么就没有利用COM的特性,实际上是直接调用的对象,就需要对数据进行保护,而如果进行了列集,那么就COM 就会自动对接口的调用进行序列化,STA套间就会保证COM对象的顺序调用,即使不同
的线程同时调用方法,套间也会保证COM对象的调用实际是在套间内线程调用的, COM对象所在的套间是STA套间,则就一个线程,如果是MTA,则必须对COM对象进行数据,防止多个MTA内线程同时调用。

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值