您的位置:首页 > 其它

COM的线程模型

2006-10-27 15:47 288 查看
COM的线程模型
COM并没有定义新的进程和线程模型,而是直接使用了Win32的线程(至少目前还是这样,还没有谁在Unix/Linux下开发出COM库来)。所以,在COM中对多线程的同步操作,都是使用操作系统提供的同步原语(比如windows下的临界区)来实现的。
要了解COM(Component Object Model,组件对象模型)的线程模型,首先要理解COM的套间(Apartment)概念。COM中有如下两种套间模式。
l 单线程套间(STA: Single-Threaded Apartment)
l 多线程套间(MTA:Multithreaded Apartment)
套间只是一个抽象的逻辑概念,它既不是进程也不是线程,但是它又有一些进程和线程的特性,它是一组对象的逻辑组合。正因为它是概念上的,因此你也可以违背它,不过这样你就不能获得COM提供的自动同步调用及兼容等好处了。每个COM对象都属于某一个套间,而不可能同时属于多个套间。但多个COM对象可以共享同一个套间。
套间与进程、线程的关系请参考图1。


图1 <TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done"> <TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done"> <TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
线程02
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
MTA(每个进程中最多只能有一个MTA)
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
线程11
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
线程12
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
线程13
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
线程14
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
STA
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
线程03
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
STA
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
线程04
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
Main STA
(第一个被创建的STA为Main STA)
</DIV></TD></TR></TBODY></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TBODY><TR><TD style="BORDER-RIGHT: #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV twffan="done">
线程01
</DIV></TD></TR></TBODY></TABLE>

从图中我们可以看到,一个进程中最多只能有一个MTA,一个MTA中可以同时存在多个线程。一个进程中可以有多个STA,但一个STA中只能存在一个线程。同时第一个被创建的STA又被称为Main STA。COM对象只有在套间中才能被创建并使用。那么如何让一个线程进入某种套间呢,这是使用以下两个API函数来实现的:
l HRESULT CoInitialize(LPVOID pvReserved);
l HRESULT CoInitializeEx(LPVOID pvReserved, DWORD dwCoInit);
要让线程退出某个套间,都必须调用void CoUninitialize(void);API函数来实现。
当一个线程已被初始化为STA,那么它就不能重初始化为MTA,除非先调用CoUninitialize来退出套间。同样,当一个线程被初始化为MTA,也要先调用CoUninitialize退出套间后,才能再初始化为STA。

接下来,我们还要弄清两个概念:COM对象和COM服务器。通常我们把一个正确注册的COM类称为COM服务器(也有称为COM组件的);我们使用COM对象的程序称为客户程序。COM服务器又可分为进程内服务器(in_process server)和进程外服务器(out-of-process server)。进程内服务器是以DLL形式物理存在并被注册的;进程外服务器是以可执行程序形式物理存在并被注册,如果在NT系统下,还可以系统服务的形式物理存在。对于进程内服务器,在客户程序会将该服务器装载入自己的进程空间,装载成功后就可从该服务器访问指定的COM对象,这里的COM对象又可分为单线程对象和多线程对象。这是COM对象自身的属性,与调用它的客户线程属性无关。
下面,我们从客户程序和COM对象的角度来分析COM的线程模型和使用方法。
我们知道在windows下一个程序被执行时,它是以一个进程的形式存在于系统中的。Windows下的进程是一个空间的概念,包含了要执行程序的数据、资源和执行代码。客户进程要使用COM对象,就必须在自己的进程空间内创建供COM对象创建和执行的套间环境。
如果客户进程的某个线程以如下方式成功调用了CoInitialize或CoInitializeEx:
CoInitialize(NULL);
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
这就意味着这个线程将在一个独享的STA中执行。
如果客户进程第一次如下成功调用了CoInitializeEx:
CoInitializeEx(NULL, COINIT_MULTITHREADED);
这就意味着该进程中创建了唯一的MTA,并且当前调用线程将在该MTA中执行。当客户程序中再有其他线程如上初始化COM,那么这些线程也将在这个唯一的MTA中执行,而不是再创建新的MTA来执行。
当套间通过以上方式创建成功后,接下来客户线程就可以从载入的COM服务器创建和使用所需的COM对象了(具体的调用方法后面会讲)。一旦线程退出了套间,那么在套间内创建的COM对象,就不再有效,这时对该对象的任何调用都会返回失败。
前面提到COM对象分为单线程对象和多线程对象两种。这两种属性是在COM接口被开发时就约定的。如果用VC的ATL库来开发COM接口时,会有Single、Apartment、Free和Both四个属性选项。
u Single
以该属性开发的接口,其对象只能被创建于客户进程的Main STA中。此对象是一个单线程对象。由于这种对象只可能在Main STA中被访问,所以不用同步全局,静态或者实例数据的访问就能保护对象。
u Apartment
以该属性开发的接口,其对象可以被创建于客户进程的任何STA中。此对象是一个单线程对象。无需同步对实例数据的访问,就能保护该对象。但由于对象可以在不同的STA中被创建,所以对全局和静态数据仍然需要同步。
u Free
以该属性开发的接口,其对象只能被创建于客户进程的唯一MTA中。此对象是一个多线程对象。由于该对象可以被多个线程共享,所以对象必须同步对实例、全局和表态数据的访问。
u Both
以该属性开发的接口,其对象总是与客户的调用线程处于同一套间。如果客户线程处于一个STA中,那么这个对象也将被创建到同一STA中,这时对象为一个单线程对象,对象的同步也将采用和该STA一样的同步方式。如果客户线程处于一个MTA中,那么这个对象也将被创建到这个MTA中,这时对象为一个多线程对象,对象的同步也将采用和该STA一样的同步方式。
可以在注册表中如下键值查看到COM对象的线程属性
[HKCR/CLSID/{CLSID}/InprocServer32/] ThreadModel=<threadmodel>
ThreadModel取值可以为空、Apartment、Free和Both,分别代表上述四种属性。
当客户线程被初始化到一个套间后,什么样的COM对象实例是可以被该线程创建和使用的呢?这就要视COM对象的以上四个属性来定了。这在前面的属性中已经提到了,只不过这里再来从客户线程的角度再阐述一遍。
如果客户线程进入的是一个STA,那么只有COM对象为Single、Apartment和Both时才有可能成功创建,并被该客户线程正确调用。其中对于为Single的COM对象,要求更苛刻,只有客户线程为Main STA时,此种COM对象才能被成功创建和调用。如果此时客户线程企图创建一个为Free的COM对象,那么创建时就会直接返回失败,因为COM对象为Free,表示该对象为多线程对象,与STA的类型是不兼容的。
如果客户线程进入的是一个MTA,那么只有COM对象为Free和Both时才有可能成功创建,并被该客户线程正确调用。如果此时客户线程企图创建一个为Single或Apartment的单线程COM对象,那么创建时就会直接返回失败,这也是由于COM对象的线程属性与客户线程所在的套间类型不兼容所致。
在实际应用中,会碰到一个套间中的线程要访问另外一个套间中的COM对象,这时需要使用COM的跨套间访问机制。跨套间访问既涉及到进程内也涉及到两个进间之间的跨套间访问,这里我们着重讲进程内的跨套间访问机制。
假设在一个客户进程中有两个套间(STA或MTA),这两个套间中分别有一个已成功创建的COM对象。由于两个COM对象分属于不同的套间,一个套间是不能直接访问另一个套间的COM对象的,这时就要使用一种称为列集(marshal)/散集(unmarshal)的技术来实现。这就要用到如下两个API:
STDAPI CoMarshalInterface(IStream * pStm, REFIID riid, IUnknown * pUnk, DWORD dwDestContext, void * pvDestContext, DWORD mshlflags);
STDAPI CoUnmarshalInterface(IStream * pStm, REFIID riid, void ** ppv);
CoMarshalInterface以一个接口指针作为输入,然后把该指针序列化之后的表示写到一个由调用者提供的字节流中。字节流的内容唯一标识了对象和它所拥有的套间。然后这个字节流可被传递到另一个套间中,在那里,CoUnmarshalInterface使用这个字节流,返回一个接口指针,在语义上它完全等同于原来的对象。接下来调用套间可以通过这个接口指针,合法的访问原来的对象。由于实际应用中,同一进程内跨线程的套间访问还是比较多的,为了简化操作,MS另提供了两个API:
HRESULT CoMarshalInterThreadInterfaceInStream(REFIID riid, LPUNKNOWN pUnk, LPSTREAM* ppStm);
HRESULT CoGetInterfaceAndReleaseStream(LPSTREAM pStm, REFIID riid, LPVOID * ppv);
实际操作中,首先把被调用的COM对象的接口指针从它所在的套间中通过CoMarshalInterThreadInterfaceInStream列集为一个字节流,然后调用套间再通过CoGetInterfaceAndReleaseStream将这个字节流返回一个接口指针,这就完成了散集。调用套间就可以使用这个指针来访问这个COM对象。实际上这种访问只是把方法调用请求转发到对象所在的套间中,而不是在调用套间中实际调用对象。在实际的开发中,很多情况下并不需要我们显示的来进行列集/散集的操作,而是有很多工作由COM库来隐式的完成了。但是不管是隐式还是显示的列集/散集操作,往往和代理/存根对象是分不开的。这时是需要我们先生成代理/存根组件来解决的,如果没有这个组件,以上关于列集/散集操作很有可能是不会成功的。对于这种组件的技术细节,还是放在COM的分布式应用中来讲比较好,这里只简单提一下。
说到这里,要再提一下进程内服务器和进程外服务器。对于进程内服务器,跨套间的调用只会出现在一个客户进程之内,主要也集中在跨线程的调用上。而对于进程外服务器,一定会涉及到跨套间的调用。但其中还要涉及到LPC,RPC等一些技术的联合运用。对于这些技术的详细介绍,这里只点到为止,可以在讲COM的分布式应用时来详细讲解。
至此已粗略的介绍了一遍COM的线程模型,并讲了一些实际的做法。后面附上例子程序,希望能对大家熟练运用COM,有所帮助。



原文出处:http://www.whzsmm.comShowPost.asp?ThreadID=185

程序进程
</DIV></TD></TR></TBODY></TABLE>
STA
</DIV></TD></TR></TBODY></TABLE>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: