Processes, Threads, and Apartments

文章开头以清晰明确的语言介绍的Process和Thread的概念。

COM引入了Apartment的概念,属于一个Apartment的线程不能直接方法另一个Apartment的内容。要访问的话必须通过proxy。有两种Apartment:

  • 支持单线程apartment,需要以同步的方式调用此apartment上的COM接口方法。会使用到windows message queue。这个叫做apartment threading。
  • 支持多线程apartment,此apartment里面的COM对象必须要自己协调多个线程对自己的调用。这个叫做free threading,和apartment threading做区分。

一个进程可以有0到多个单线程apartment,但是只能有0到1个多线程apartment。

远程COM调用(跨进程或者跨网络)的时候,调用者和被调用的线程模型不用做特别关系。如果是进程内调用,那么有一些注意事项,参考In-Process Server Threading Issues

Single-Threaded Apartments

一个单线程的Apartment具有一个apartment model process,带有一个消息队列(GetMessage/DispatchMessage)。对于这个Apartment的请求会被序列化到这个消息队列上。在一个单线程的Apartment里面,接口指针可以随意传递,不需要编组(marshaling)。在单线程Apartment中的对象可以自由沟通。如果需要同步的话,MsgWaitForMultipleObjects 可以用来等待消息,以及其他线程同步事件。

apartment model process中的apartment model object不能在其他线程中执行,只能在自己的apartment model process的上下文中执行。对于来自远程的调用,COM proxy可以帮助把上下文切换到相应的apartment中。

在同一个进程中,在不同的apartment中通信,也要表现得跟远程一样,需要把请求编组,然后投递到消息队列进行分发。

文档中总结得一套关于单线程apartment的规则:

  • Every object should live on only one thread (within a single-threaded apartment).
  • Initialize the COM library for each thread.
  • Marshal all pointers to objects when passing them between apartments.
  • Each single-threaded apartment must have a message loop to handle calls from other processes and apartments within the same process. Single-threaded apartments without objects (client only) also need a message loop to dispatch the broadcast messages that some applications use.
  • DLL-based or in-process objects do not call the COM initialization functions; instead, they register their threading model with the ThreadingModel named-value under the InprocServer32 key in the registry. Apartment-aware objects must also write DLL entry points carefully. There are special considerations that apply to threading in-process servers. For more information, see In-Process Server Threading Issues.
  • While multiple objects can live on a single thread, no apartment model object can live on more than one thread.

对于apartment model process,需要调用CoInitialize或者CoInitializeEx来初始化。在不同的apartment传递接口指针的时候需要将其编组,可以通过CoMarshalInterThreadInterfaceInStream来编组,以及CoGetInterfaceAndReleaseStream来解编组。这两个方法底下调用的是CoMarshalInterface和CoUnmarshalInterface方法,并且设置标志位MSHCTX_INPROC。

通常情况下COM会自动编组和解编组接口,比如调用CoCreateInstance的时候。如果应用程序通过COM意外的机制来传递接口指针,那么就需要应用程序自己来编组。一个例子,如果Apartment 1提供了一个接口指针给Apartment 2使用,Apartment 1必须先调用CoMarshalInterThreadInterfaceInStream 来编组接口。编组后的接口是线程安全的,可以放置到一个变量中供Apartment 2访问。Apartment 2必须通过CoGetInterfaceAndReleaseStream 来解编组这个接口,获得一个指向接口proxy的指针。

对于每个单线程apartment,COM会创建一个隐藏的窗口,类别是OleMainThreadWndClass。对于此COM对象的调用会转为为此隐藏窗口的消息。IMessageFilter可以允许在apartment中取消或者筛选消息。

Multithreaded Apartments

一个程序可以有一个多线程的Apartment,这个Aparment就像是线程的天堂,这里的线程不受管制,线程之间传递消息不需要对接口进行编组。这个Apartment也没有必要使用窗口消息。Apartment中的COM对象可以在任意线程上执行。

但是呢,自由的代价也是显而易见的,任何需要同步的操作都需要程序自己实现。需要使用event、critical section、mutex以及semaphore等机制实现同步。另外,COM对象不拥有其所运行的线程,所以不能再COM对象中保存任何和线程有关的状态,不能把东西存在thread local storage。

值得注意的规则:

  • COM provides call synchronization for single-threaded apartments only.
  • Multithreaded apartments do not receive calls while making calls (on the same thread).
  • Multithreaded apartments cannot make input-synchronized calls.
  • Asynchronous calls are converted to synchronous calls in multithreaded apartments.
  • The message filter is not called for any thread in a multithreaded apartment.

可以通过使用COINIT_MULTITHREADED调用CoInitializeEx来一个线程设置成自由线程,从而归属到多线程的Apartment中。当一个自由线程内的客户端进行远程COM调用的时候,会挂起,直到调用结束才能恢复。

进程内线程自由的COM对象可以接收任意来自其客户端的调用。而进程外的COM服务端则需要在proxy侧维护一个线程池来管理对COM对象的调用。

若干同步机制:

  • Event。有点像pthread中的condition_var,用来实现事件通知
  • Critical section,限制同一进程内的不同线程对关键区的访问,类似pthread的mutex
  • Mutex,和Critical section类似,不过可以作用于不同进程的线程
  • Semaphore,以计数的方式支持线程同步

In-Process Server Threading Issues

进程内的COM服务端不需要通过CoInitialize, CoInitializeEx, 或者OleInitialize来设置自己的线程模型。对于基于DLL或者进程内的COM服务端,需要在注册的时候添加 ThreadingModel标记到InprocServer32键(注册表相关)。

只有使用进程内的方式,才能使一个COM对象和其创建者在同一个Apartment。

ThreadingModel可以有几种类型:

  • 在MTA内为None
  • 在STA内为Aparment
  • 在MTA内为Free
  • 也可以为Both,表示可以在STA或者MT

对于DLL方式的话,必须实现和导出函数DllGetClassObject和DllCanUnloadNow。当一个客户端需要使用到DLL中的接口时,通过CoGetClassObject(或者直接调用CoCreateInstance)来调用DllGetClassObject 来获得一个接口指针。在不同情况下DllGetClassObject 可以给出一个类的多个实例,或者一个实例的多个引用(使用 InterlockedIncrement/InterlockedDecrement来管理引用)。DllCanUnloadNow则是在DLL被卸载的时候调用,比如在CoFreeUnusedLibraries()被调用时。

好吧。这一章有点小复杂。访问同一进程内的COM服务端可以走一些捷径来提高性能。比如跨过代理直接访问COM服务端。但是这样做容易出错,有一些规则需要遵守。

Accessing Interfaces Across Apartments

在同一进程的COM对象可以通过IGlobalInterfaceTable来发布接口,这个接口提供三个方法:

  • 注册一个全局接口
  • 获取一个其他apartment的接口,并获得一个cookie
  • 取消注册一个全局接口

COM有一类对象成为agile对象,也就是说执行的时候不用关心所在线程以及上下文,包括apartment。IGlobalInterfaceTable可以使agile对象获取其他接口指针的时候更加没有限制,不用关系底下是proxy还是直接访问。

如果一个COM对象把接口指针传递给了其他Apartment的COM客户端使用,那么可能会产生这么一种情况。如果这个接口的函数调用了COM对象实现的一个成员接口的方法,可能会出问题。因为这个成员接口不能在其他Apartment被调用。一个改善的方法是把成员接口注册到IGlobalInterfaceTable,然后保留其Cookie。需要的时候使用Cookie通过IGlobalInterfaceTable::GetInterfaceFromGlobal去获取该接口,而不是直接调用。

使用 IGlobalInterfaceTable的一个好处是不用总是编组和解编组接口。

如果直接在线程之间传递接口,可以使用CoMarshalInterThreadInterfaceInStream,只需解编组一次。

Creating the Global Interface Table讲述如何获取IGlobalInterfaceTable。

Understanding COM Apartments, Part I Understanding COM Apartments, Part II A Simple Example To Explain COM STA, MTA, and Auto-threaded Modules Threading and Tasks in Chrome 6 COM Threading Models

(完)