COM 线程模型
每一个用户界面线程都
有一个或多个窗口。线程将调用它所拥有的窗口的窗口过程。对于任一窗口, 它将被创
建它的线程所拥有。所以窗口过程总是在同一线程中被执行的, 而不论被处理的消息是
哪个线程发出的。这样做的结果是所有的消息被同步。窗口将可以保证它能按正确的顺
序获取各个消息。
这样, 程序员将用不着编写线程安全的窗口过程.
COM 线程
与Win32 中的用户界面线程相对应,COM 有一个‘套间线程’
而与工作线程相对应的则是一个自由线程。
套间
它实际上是一个由用户界面风格的线程(套间线程) 和一个消息循环构成的概念性实体。
除了可以表示一个线程外,下图也可以表示一个套间。
Windows 应用程序程序图示:它包括线程执行、消息循环、进程边界及程序代码。
这里我们看到,这里包括了用户界面风格的线程(这个用户界面风格的线程,我理解,就是包含了一个消息循环的线程)以及一个消息循环。
下图是一个比较经典的情况:
一个客户,加载了两个组件,而组件们没有自己的线程,它们实际就是被加载,然后被客户主动在进程内调用的。此时,套件线程,就是客户的自己的消息循环的线程。此时,这样的程序与没有使用任何COM 组件的程序相比,最大的区别在于,程序在使用任何COM 库服务之前必须先调用CoInitialize,而在退出之前调用CoUninitialize。
进程外组件的情况:
每个进程都有它自己的执行线程。进程外服务器有它自己的消息循环。
跨越进程边界的函数调用的参数必须被调整。
下图包含两个套间,一个是客户,而组件则在另外的套间中。
当然,当上图中的进程外组件被修改为一个在另外一个套间的进程中组件,此处长虚线框表示的是不同的套间,而点线仍然表示进程边界。
跨越进程或套间边界(实际跨越的是同步的边界)的函数调用的同步将通过消息循环而得以实现。
套间线程
指的是套间中仅有的唯一线程。可以将其想象为一个用户界面线程。
套间线程的服务对象是其创建的组件。一个套间中的组件只能由相应的套间线程调用。
假定另外一个线程调用一个套间中某个组件的方法,COM 将把此调用请求放到套间的队列中。消息循环将取出此调用请求并在套间线程上执行相应的方法。
由于COM 将保证所有对组件的调用均被同步,因此组件无需是线程安全的。
自由线程
如果某个组件是由自由线程创建的,则任意线程均可在任意时候调用它。此时组件开发者应该保证对组件访问的同步。此时组件应该是线程安全的。
调整与同步
在使用套间线程的情况下, 所有的调整与同步处理将由COM 完成。而在自由线程的情况下, 调整处理可能是不需要的, 而同步则需要组件自己完成.
- 进程间的调用将被调整,
- 同一线程中的调用将不被调整
- 对于套件线程中组件的调用将被调整
- 对于自由线程中组件的调用并不是总被调整
- 对于套间线程的调用将被同步
- 对于自由线程的调用将不被同步
- 同一线程中的调用将由此线程本身完成同步
除非特别指明,所有线程将在同一进程中运行
一:同一线程中的调用
一切正常
二:套间线程对套间线程的调用
COM 将对此调用进行同步,COM 也将对接口进行调整,某些情况下,需要程序员自己完成对套间线程间接口的调整。
三:自由线程对自由线程的调用
COM 不进行同步,此调用将在客户线程中被执行,此时组件需要同步对它的访问,当两个线程在同一个进程中,调用将不被调整。
四:自由线程对套间线程的调用
COM 对此调用进行同步,组件将在套件线程中被调用。而且不论此时这两个线程是否在同一进程中,都需要对接口进行调整。大多数情况下,将由COM 完成,但在某些情况下,需要Coder 完成调整。
五:套件线程对自由线程的调用
同步操作由组件完成。接口将被调整。同一进程,COM 回对调整进行优化,以将指针直接传给客户。
套间线程的实现
使用套间中的组件的最大的好处在于这些组件无需是线程安全的,COM 将同步对套间中组件的访问,这些访问可以是来自套间线程的,也可以是来自自由线程的。
内部实现上来看,COM 将使用一个隐藏的Windows 消息队列来同步这种调用。套间的一些要求:
- 必须调用CoInitialize 或 OleInitialize
- 只能有一个线程
- 必须有一个消息循环
- 在将接口指针传给其它套间时,必须对之进行调整
- 若套间是一个进程中组件,则其DLL 入口点必须是线程安全的
- 可能需要一个类型安全的类厂
组件只能运行于一个线程中。单线程套间中的组件只能运行于一个线程中。此种组
件只能供创建它的线程访问。但组件仍应保护其全局数据, 因为同窗口过程一
样, 它是可重入的。
在跨越套间边界时必须对接口进行调整。从其他线程发来的调用请求必须被调整,
以使它们像是从组件所在线程中发出的那样。由于一个套间只有一个线程, 因此所有其
他线程都在此套间之外。跨越套间边界的调用必须被调整。关于接口指针的调整, 我们
将在稍后讨论。
DLL 入口点必须是类型安全的。单线程套间中的组件无需是线程安全的, 因为它们
只会被创建它们的线程所调用。但进程中服务器DLL 中的入口点, 如DllGetClassObject
和DllCanUnloadNow 则必须是线程安全的。这是因为多个不同线程中的多个客户可能
会同时调用它们。为使这些函数是线程安全的, 需要同步对共享数据的访问。在某些情
况下, 这意味着类厂也必须是线程安全的。
类厂可能需要是线程安全的。若为所建立的每一个组件都创建一个不同的类厂, 那
么这些类厂无需是线程安全的, 这是因为同一时刻将只会有一个客户来访问它们。但若
DllGetClassObject 只创建一个类厂并用它来创建所有的组件实例, 则将需要保证此类厂
当一个套间线程中的组件将其接口指针传递给另外一个线程中的客户时, 此接口指
针必须调整, 不论接收线程是套间线程还是自由线程
自动调整
在许多情况下, COM 将自动为程序员进行接口的调整.从程序员的角度来看, 线程并没有改
变这些代理/ 残根DLL 的作用。它们将自动完成跨越进程边界的调整。
COM 也使用这些代理/ 残根DLL 来完成套间线程和同一进程中其他线程之间的接
口调整。所有当访问另外某个套间中组件的接口时, COM 将自动地通过此代理来进行此
种调用, 这样接口将被相应地进行调整。
手工调整
在跨越套间边界但并没有通过COM 进行通信时,需要程序员自己对接口指针进行调整。
例子:
- 客户创建一个套间线程,此线程中将创建一个组件并对之进行处理。主线程也可会访问此组件。套间线程将拥有一个此组件的接口指针。但主线程并不能直接使用此接口指针,因为它所处的套间与创建组件的套间不同。主线程如果要使用此组件,套间线程必须对相应的接口指针进行调整并将其传给主线程。而在主线程使用此接口指针之前,它必须对之进行反调整。
- 进程中组件的类厂在其他的线程中创建此组件的实例。前一个例子中,线程由客户创建,这里,组件将在不同的线程中创建它自身。客户将调用CoCreateInstance ,这最终将导致组件的类厂被创建。当客户调用IClassFactory::CreateInstance 时,类厂将创建一个新的套间线程。此新线程将创建相应的组件。IClassFactory::CreateInstance 必须对客户传回的一个接口指针。但CreateInstance 并不能将在新的套间中所创建的接口指针直接传回给客户,因为客户是在另外一个线程中。所以套间线程必须对传给CreateInstance 的接口指针进行调整,然后CreateInstance 将对此指针进行反调整并将其传回给客户。
如何调整:
- CoMarshalInterface 和 CoUnMarshalInterface
- 更好的办法:CoMarshalInterThreadInterfaceInStream 和CoGetInterfaceAndReleaseStream
为对IX 接口进行调整,使用下面的函数:
IStream* pIStream = NULL;
HRESULT hr = CoMarshalInterThreadInterfaceInStream(IID_IX,pIX,&pIStream);
反调整
IX* pIXmarshaled;
HRSULT hr = CoGetInterfaceAndReleaseStream(
pIStream,
IID_IX,
(void**)&pIXmarshaled);
这主要是由于COM 将自动在幕后为我们使用代理/残根DLL 的接口。
套间线程与同的win32 线程在这方面的不同点在于:
- 初始化COM 库
- 具有一个消息循环
- 对传回给主线程的接口进行调整
关于线程模型的注册表关键字
为登记进程中组件的线程模型, 可以给组件的InprocServer32 关键
字加上一个名为ThreadingModel 的名称值( 注意不是子关键字)。ThreadingModel 有三个可能的值:Apar tment、Free 和Both。