本页
概要
COM 对象可以用于一个进程的多线程。“单线程单元” (STA) 和“多线程单元” (MTA) 术语用于为描述对象与线程间的关系、对象间的并行关系、使用何种方法将调用传递给对象的方式以及在线程间传递接口指针的规则而建立的概念框架。组件及其客户可以在 COM 目前支持的以下两个单元模型之间进行选择:
在 NT 4.0 中有关于 MTA 的介绍,且在配备 DCOM95 的 Windows 95 中可以找到。在 Windows NT 3.51 和 Windows 95 以及 NT 4.0 和带有 DCOM95 的 Windows 95 中均有 STA 模型。
1. | 单线程单元模型 (STA):进程中一个或多个线程使用 COM ,并且 COM 对象的调用由 COM 进行同步。在线程间对接口进行编组。单线程单元模型的退化情况(其中,在给定的进程中只有一个线程使用 COM)被称为单线程模型。以前的 Microsoft 信息与文档曾经将 STA 模型简单地称为“单元模型”。 |
2. | 多线程单元模型 (MTA):一个或多个线程使用 COM,并且由所有与 MTA 有关的线程直接调用与 MTA 有关的 COM 对象,而在调用者和对象间没有系统代码的插入。由于多个同步客户可能将或多或少地同时调用对象(同时在多个处理器系统上),所以对象必须自己同步其内部状态。在线程间没有接口编组。以前的 Microsoft 信息与文档曾经将此模型称为“自由线程模型”。 |
3. | STA 模型和 MTA 模型均可以用于同一进程。有时这种模型被称为“混合模型”的进程。 |
更多信息
概述
COM 中的线程模型为使用不同线程结构的组件提供一起工作的机制。同时还为需要它们的组件提供同步服务。例如,某个特定对象可能被设计成只能由单线程调用, 并且可能无法同步来自客户的并行调用。如果这种对象被多个线程同时调用,则将导致系统崩溃或产生错误。COM 提供处理这种线程结构互操作性的机制。
即使支持线程的组件也常常需要同步服务。例如,OLE/ActiveX 控件、原有活动嵌入、和 ActiveX 文档等具有图形用户界面 (GUI) 的组件,需要对 COM 调用和窗口信息进行同步和串行。 COM 提供这些同步操作服务,所以不需要复杂的同步操作编码就能够写入这些组件。
“单元”有几个相互关连的方面。首先,从并行的角度来讲它是一个逻辑结构,例如线程与一系列 COM 对象是如何关联的。其次,它是编程人员必须遵守的一系列规则,以便可以从 COM 环境获取期望的并行操作。最后,它是系统提供的代码,用于帮助程序员管理针对 COM 对象的线程并行。
“单元”术语来自一个比喻,进程在其中被设想成一个完全离散的实体,就好像一个“大厦”被分成一系列相关而又不同的称为“单元”的“区域”一样。单元是一个“逻辑容器”,它在对象间和线程间(某些情况下)创建关联。虽然可能存在与 STA 模型中的某个单元逻辑关联的单个线程, 但线程不是单元。虽然每个对象都与一个且仅一个单元相关联,但对象不是单元。但是,单元不仅仅是一个逻辑结构;其规则描述了 COM 系统的行为。如果不遵守单元模型的规则,COM 对象将不能正常工作。
更多细节
单线程单元 (STA) 是一系列与特定线程相关的 COM 对象。这些对象通过由该线程创建(或者更精确地说,率先在该线程上暴露给 COM 系统(典型地通过编组))的方式与单元相关联。STA 被当作对象或代理“生存”的地方。如果对象或代理需要被另一个单元访问(在同一个或不同的进程中),其接口指针必须被编组到创建新代理的该单元中。如果遵守单元模型规则,则在同一进程中不允许从其它线程直接调用该对象;这将违反在指定单元内的所有对象都应在一个单线程上运行的规则。之所以有规则是因为如果在其它线程上运行,多数在 STA 下运行的代码将不能正常运行。
与 STA 相关的线程必须调用 CoInitialize 或 CoInitializeEx(NULL, COINIT_APARTMENTTHREADED),而且必须为相关的对象检索和发送窗口信息以接收传入的调用。如本文后面所讲述的,COM 使用窗口信息向 STA 中的对象发送和同步调用。
“主 STA”是在给定进程中首先调用 CoInitialize 或 CoInitializeEx(NULL,COINIT_APARTMENTTHREADED)的线程。如本文后面所述,由于一些进程内对象总是在主 STA 中加载,所以进程的主 STA 必须始终保持有效直到所有 COM 工作完成为止。
Windows NT 4.0 和 DCOM95 引进了一种新型单元,即多线程单元 (MTA)。MTA 是与进程中一系列线程关联的 COM 对象,以便任何线程可以直接调用任何的对象执行,而无须插入系统代码。 MTA 中指向任何对象的接口指针可以不必经过编组而通过在与 MTA 相关的线程间进行传递。进程中调用 CoInitializeEx(NULL, COINIT_MULTITHREADED)的所有线程均与 MTA 有关。与上述 STA 不同, MTA 中的线程不需要为相关对象检索和发送窗口信息来接收传入的调用。COM 不对 MTA 中的对象的调用进行同步。MTA 中的对象必须保护自己的内部状态以防止由于多个同时线程的相互作用而被损坏,且这些对象不能对在不同方式调用间保持不变的“线程本地存储”的内容做任何假设。
一个进程可以有许多 STA,但最多只能有一个 MTA。MTA 包含一个或多个线程。每个 STA 只有一个线程。一个线程最多只能属于一个单元。对象只能属于一个单元。接口指针应该总是在单元间进行编组(虽然编组的结果可能是一个直接指针而不是一个代理)。请参见下面有关 CoCreateFreeThreadedMarshaler 的信息。
进程选择由 COM 所提供的线程模型中的一种。一个 STA 模型进程含有一个或多个 STA,并且没有 MTA。一个 MTA 模型进程含有带一个或多个线程的一个 MTA,并且没有任何 STA。一个混合模型进程含有一个 MTA 和任何数目的 STA。
单线程单元模型
STA 的线程必须调用 CoInitialize 或 CoInitializeEx(NULL, COINIT_APARTMENTTHREADED),且必须检索和发送窗口消息,这是因为 COM 使用窗口消息来同步和发送本模型中的对象调用。请参见本文下边的参考资源章节,以获取更多信息。
支持 STA 模型的服务器:
在 STA 模型中,对象调用是由 COM 按与将窗口消息传送到窗口的同一方法进行同步的。使用窗口消息将调用传递到创建该对象的线程上。因此,该对象的线程必须调用 Get/PeekMessage 和 DispatchMessage 以接收调用。COM 创建与每个 STA 关联的隐藏窗口。STA 外的对象调用是通过使用投递到该隐藏窗口的窗口消息由 COM 运行时传输到该对象线程。当与对象的 STA 相关的线程进行检索并发送消息,隐藏窗口的窗口例程(也由 COM 实现)接收该消息。由于 COM 运行时同时在从 COM 所属线程到 STA 线程的调用的双方上,所以 COM 运行时使用窗口例程来“吸引”与 STA 相关的线程。COM 运行时(目前运行在 STA 线程上)通过一个 COM 提供的存根“向上”调用相应的对象接口方法。从该方法调用返回的执行路径反向“向上”调用;该调用返回到存根区域和 COM 运行时,它窗口消息将控件传递回的 COM 运行时,然后通过 COM 通道返回到原始调用者。
当多个客户调用 STA 对象时,这些调用通过在 STA 中所使用的控件传输机制而自动排列在消息队列中。每次当对象的 STA 检索和发送信息时, 都接到一个调用。由于按此方法由 COM 对调用进行同步,而且因为调用总是在与对象的 STA 相关的单线程上进行传送,所以对象的接口实现不必提供同步。
备注:如果当接口方法实现在处理某个方法调用时检索并发送信息,从而引起另一个调用由同一个 STA 传递到该对象,则可以重新输入对象。发生这种情况的常见原因是,如果 STA 对象使用 COM 进行传出(跨单元/跨进程)调用。这与如果在处理信息的同时检索并发送信息,则窗口例程可以被重新输入的方式相同。虽然 COM 不阻止在同一线程上的重新输入,但可以阻止并行操作。另外,它还提供一种可以管理与 COM 相关的重输入方式。请参见本文下边的参考资源章节,以获取更多信息。如果方法实现不在对象单元外进行调用或另外检索和发送信息,则不重新输入对象。
STA 模型中的客户职责:
使用 STA 模型的进程和/或线程中运行的客户代码必须通过使用 CoMarshalInterThreadInterfaceInStream 和 CoGetInterfaceAndReleaseStream 对单元间的对象接口进行编组。例如,如果客户中的单元 1 有接口指针,单元 2 要使用它,则单元 1 必须用 CoMarshalInterThreadInterfaceInStream 对该接口进行编组。由此函数返回的流对象是线程安全的,且其接口指针应存储于一个可由单元 2 访问的直接内存变量中。单元 2 必须通过这个到 CoGetInterfaceAndReleaseStream 的流接口,以便不用对下面的对象接口进行编组,并将指针送回可以通过它来访问该对象的代理上。
由于某些进程内对象加载在主单元内,所以特定进程的主单元必须始终保持有效,直到客户已经完成所有的 COM 工作为止。(详细信息阐述如下)。
多线程单元模型
MTA 是由所有已调用 CoInitializeEx(NULL, COINIT_MULTITHREADED) 的进程中的所有线程创建或暴露的对象集合。
备注:COM 的当前实现允许将未明确初始化 COM 的线程作为 MTA 的一部分。只有在进程中至少有一个其它线程曾经调用过 CoInitializeEx(NULL, COINIT_MULTITHREADED) 之后才开始使用 COM 的情况下,未初始化 COM 的线程才能用作 MTA 的一部分。(当没有客户线程已经明确初始化 MTA 时,COM 甚至有可能自己初始化 MTA。例如,与 STA 相关的线程在标为 ThreadingModel=Free 的 CLSID 上调用 CoGetClassObject/CoCreateInstance[Ex],而 COM 暗中创建将类对象加载到其中的 MTA。)请参见以下有关线程模型互操作性的信息。
但这是一个可能引起问题的配置,例如某些情况下的违规访问。因此,强烈建议每个需要进行 COM 工作的线程都通过调用 CoInitializeEx 对 COM 进行初始化,然后在完成 COM 工作时调用 CoUninitialize。“不必要”初始化 MTA 的成本最低。
由于 COM 在此模型中未使用窗口消息传递对象调用,所以 MTA 线程不需要检索和发送信息。
支持 MTA 模型的服务器:
在 MTA 模型中,对象调用没有通过 COM 进行同步。多个客户可以在不同的线程上并行调用支持此模型的对象,且对象必须在其接口/方法实现中使用诸如事件、多路同步信号、标记等同步对象来提供同步。MTA 对象可以通过属于该对象进程的 COM 所创建的线程池接收来自多个进程外客户的并行调用。MTA 对象可以接收来自与 MTA 相关的多线程上的多个进程内客户的并行调用。
MTA 模型中的客户职责:
在使用 MTA 模型的进程和/或线程上运行的客户代码没有必要在其自身和其它 MTA 线程间对对象的接口指针进行编组。而且,一个 MTA 线程可以使用来自另一个 MTA 线程的接口指针作为直接内存指针。当客户线程调用进程外的对象时,它处于暂停状态直到该调用完成为止。当所有与 MTA 相关的应用程序创建的线程被在传出调用上被阻塞时,调用可能会到达与 MTA 相关的对象。在此种情况及一般情况下,传入的调用被传递到由 COM 运行时所提供的线程上。在 MTA 模型中没有消息筛选器 (IMessageFilter) 可以使用。
混合线程模型
支持混合线程模型的进程将使用一个 MTA 以及一个或多个 STA。接口指针必须在所有单元间编组,但在 MTA 内可以不经过编组而使用。STA 中的对象调用由只在一个线程上运行的 COM 进行同步,而在 MTA 中对的对象象则不是这样。但是,由 STA 向 MTA 的调用通常经过系统提供的代码,并在传递到对象之前由 STA 线程切换到 MTA 线程。
备注:有关在何处可以使用直接指针的情况,STA 线程如何直接调入首先与 MTA 相关的对象以及怎样从多个单元调出的信息,请参见 SDK 文档上有关 CoCreateFreeThreadedMarshaler() 的内容以及如下有关该 API 的讨论。
选择线程模型
组件可以选择支持 STA 模型、MTA 模型或使用混合线程模型支持这两种模型的组合。例如,执行大量 I/O 的对象可以选择支持 MTA,这样便可通过允许在 I/O 等待时间内进行接口调用,为客户提供最大程度的响应。或者,与用户相互作用的对象几乎总是选择支持 STA,以便用其 GUI 操作同步传入的 COM 调用。由于 COM 提供同步,因此支持 STA 模型比较容易。由于对象必须实现同步,因此支持 MTA 模型比较困难,但因为同步只是用于小部分代码,而不是用于由 COM 所提供的整个接口调用,所以到客户的响应较好。
由于 STA 模型也用于 Microsoft Transaction Server(MTS,以前的代码名为“Viper”),因此计划在 MTS 环境中运行的基于 DLL 的对象应该采用 STA 模型。为 MTA 模型实现的对象通常在 MTS 环境中可以工作正常。但是,由于它们将使用不必要的线程同步基本要素,所以不能很有效地运行。
标记进程内服务器的支持线程模型
如果线程未经初始化调用 CoInitializeEx(NULL, COINIT_MULTITHREADED) 或使用 COM,则使用 MTA 模型。如果线程调用 CoInitialize 或 CoInitializeEx(NULL, COINIT_APARTMENTTHREADED),则使用 STA 模型。
由于 COM 运行时的启动代码可以按需要的形式初始化 COM,因此 CoInitialize API 为客户代码和打包进 .EXE 中的对象提供单元控件。
但是,由于那些 API 在服务器被加载时将已被调用,所以进程中(基于 DLL)的 COM 服务器不调用 CoInitialize/CoInitializeEx。因此,DLL 服务器必须使用注册表来通知 COM 其所支持的线程模型,以便 COM 可以保证系统按一种与之兼容的方式运行。为此,称为 ThreadingModel 的组件的 CLSID/InprocServer32 项的命名值按如下方式进行使用:
• | ThreadingModel 数值不存在:支持单线程模型。 |
• | ThreadingModel=Apartment:支持 STA 模型。 |
• | ThreadingModel=Both:支持 STA 和 MTA 模型。 |
• | ThreadingModel=Free:仅支持 MTA 模型。 |
进程内服务器的线程模型将在本文的后面进行讨论。如果进程内服务器提供许多对象类型(每种类型都有自己唯一的 LSID),则每种类型都可以拥有一个不同的 ThreadingModel 值。换而言之,线程模型根据是 CLSID,而不是根据代码包/DLL。但是,自举和查询所有进程内服务器 (DLLGetClassObject(),DLLCanUnloadNow()) 所必需的 API 进入点必须对于任何支持多线程(即单元的 ThreadingModel 值,两者或自由)的进程内服务器是线程安全的。
如前所述,进程外服务器没有用 ThreadingModel 对自身进行标记。而是使用了 CoInitialize or CoInitializeEx。期望用 COM “代理”功能(如系统提供的代理 DLLHOST.EXE)在进程外运行的基于 DLL 的服务器只要简单地遵循基于 DLL 的服务器规则即可;在这种情况下不需要有任何特殊考虑。
客户和对象使用不同的线程模型时
即使在由于客户和对象位于不同的进程而使用了不同的线程模型,且 COM 涉及到从客户向对象传递调用的情况下,客户和进程外对象间的相互作用也是非常简单。由于 COM 被插入到客户与服务器之间,所以它为线程模型间的相互操作提供了代码。例如,如果 STA 对象由多个 STA 或 MTA 客户并行调用,COM 将通过在服务器消息队列中放置相应的消息窗口来同步这些调用。每次对象的 STA 进行检索和发送消息时, 都将接到一个调用。线程模型互操作性的所有组合都可以在客户和进程外对象之间进行并获得全面支持。
客户与使用不同线程模型的进程内对象之间的相互作用比较复杂。虽然服务器在进程中,但有时 COM 必须将自己插入到客户和对象之间。例如,应该支持 STA 模型的进程内对象可能会被客户的多线程并行调用。由于该对象不是专为这种并行访问而设计的, 所以 COM 不允许该客户线程直接访问对象的接口。取而代之,COM 必须保证调用经过同步,并且只能由与“包含”该对象的 STA 相关的线程进行调用。尽管存在这些附加的复杂性,仍然允许在客户与进程内对象之间进行所有线程模型互操作性的组合。
进程外(基于 EXE)服务器中的线程模型
以下是进程外服务器的三种类型,每一种都可以由任何 COM 客户进行使用,而不管该客户使用的是哪种线程模型:
1. | STA 模型服务器: 服务器在一个或多个 STA 上进行 COM 工作。传入的调用是由 COM 进行同步的并由与在其中创建对象的 STA 相关的线程进行传送。类集方法调用是通过与注册类集 STA 相关的线程进行传送的。对象和类集不需要实现同步。但是,实现程序必须对任何由多个 STA 使用的全局变量的访问进行同步。服务器必须使用 CoMarshalInterThreadInterfaceInStream 和 CoGetInterfaceAndReleaseStream 对 STA 之间的接口指针(可能来自其它服务器)在 STA 间进行编组。服务器可以有选择地在每个 STA 中实现 IMessageFilter 以控制由 COM 调用传送的方式。一个退化的情况是在一个 STA 中进行 COM 工作的单线程模型服务器。 |
2. | STA 模型服务器: 服务器在一个或多个线程上进行 COM 工作,所有这些线程均属于 MTA。调用没有经过 COM 进行同步。COM 在服务器进程中创建线程池,并由这些线程中的任何线程传递客户调用。线程不必检索和发送信息。对象和类集必须实现同步。服务器不需要对线程之间的接口指针进行编组。 |
3. | 混合模型服务器: 有关详细信息,请参见本文中标为“混合线程模型”的内容。 |
客户中的线程模型
有三种客户类型:
1. | STA 模型客户: 客户在与一个或多个 STA 相关的线程中进行 COM 工作。客户必须使用 CoMarshalInterThreadInterfaceInStream 和 CoGetInterfaceAndReleaseStream 对 STA 之间的接口指针进行编组。一个退化情况是在一个 STA 中进行 COM 工作的单线程模型客户。客户的线程在进行传出调用时输入一个 COM 提供的消息循环。在等待传入调用和其它并行问题的同时,客户可以使用 IMessageFilter 来管理窗口消息的回调和处理。 |
2. | MTA 模型客户: 客户在一个或多个线程中进行 COM 工作,所有这些线程均属于 MTA。客户不需要对线程之间的接口指针进行编组。客户不能使用 IMessageFilter。当向进程外对象进行 COM 调用时,客户的线程暂时停止,且当该调用返回时继续。传入的调用到达 COM 所创建与管理的线程上。 |
3. | 混合模型客户: 有关详细信息,请参见本文中标为“混合线程模型”的内容。 |
进程内(基于 DLL)服务器中的线程模型:
以下是四种类型的进程内服务器,每种都可以由任何 COM 客户使用,而不管该客户使用的是何种线程模型:但是,如果想支持线程模型的互操作性,进程内服务器必须为其实现的任何自定义(非系统定义)接口提供编组代码,这是因为这种情况通常需要对客户单元间的接口进行编组。这四种类型分别为:
1. | 支持单线程(“主”STA)的进程内服务器 - 无 ThreadingModel 值: 由这种服务器提供的对象应该由在 STA 创建的同一客户 STA 进行访问。此外,服务器期望其所有的进入点如 llGetClassObject 和 DllCanUnloadNow 以及全局数据都可被同一线程(与主 STA 相关的)所访问。在 COM 中引入多线程之前存在的服务器属于这种类别。由于这些服务器不是为多线程访问而设计,所以 COM 创建由服务器在进程的主 STA 中所提供的所有对象,并且对象调用是通过与主 STA 相关的线程来传递的。其它客户单元通过代理获取对象的访问权限。来自其它单元的调用从代理进入主 STA 中的存根(线程间编组),然后再进到该对象。此编组允许 COM 同步对象调用,并且由在其中创建该对象的 STA 传递调用。由于线程间编组相对于直接呼叫较慢,因此建议重新编写这些服务器以使其可以支持多个 STA(类型 2)。 |
2. | 支持单线程单元模型(多 STA )的进程内服务器 - 标为 ThreadingModel=Apartment: 由这种服务器提供的对象应该由在 STA 创建的同一客户 STA 进行访问。因此,它与由单线程的进程内服务器所提供的对象相似。但是,由于此服务器提供的对象可以在进程的多个 STA 中进行创建,所以该服务器必须设计其进入点(如 DllGetClassObject 和 DllCanUnloadNow)以及全局数据以供多线程使用。例如,如果进程的两个 STA 同时创建进程内对象的两个实例,则 DllGetClassObject 可能会被两个 STA 同时调用。同样,必须编写 DllCanUnloadNow 以防止当代码正在服务器上执行时而服务器被卸载。 如果服务器只提供一个创建所有对象的类集实例,则必须也将类集实现设计为用于多线程,这是因为它将由多客户 STA 进行访问。如果每次调用 DllGetClassObject 时,服务器都创建一个类集的新实例,则类集不要求是有线程安全的。但是,实现程序必须对任何全局变量的访问进行同步。 类集创建的 COM 对象没有线程安全要求。但是对全局变量的访问必须由实现程序进行同步。一旦对象由线程创建,则该对象总是通过该线程被访问,并且所有对象调用由 COM 进行同步。与在其中创建对象的 STA 不同的客户单元必须通过代理来访问对象。这些代理是在客户对其单元间接口进行编组时创建的。 通过其类集创建 STA 对象的任何客户都获得一个指向该对象的直接指针。这与单线程进程内对象有所不同,前者只有客户的主 STA 获得一个指向对象的直接指针,且所有创建对象的其它 STA 获得通过代理访问该对象的权限。由于线程间编组相对于直接调用而言较慢,所以将单线程进程内服务器更改成为支持多 STA 的服务器可以显著地提高其速度。 |
3. | 只支持 MTA 的进程内服务器 - 标为 ThreadingModel=Free: 由此服务器提供的对象只对 MTA 而言比较安全。它实现其自身的同步化,并且可以由多客户线程同时进行访问。此服务器可能存在与 STA 模型不兼容的行为。( 例如,按中断 STA 消息泵的方式对窗口消息队列的使用。)另外,通过将对象的线程模型标记成“自由”,对象的实现程序按照如下所示进行:此对象可以由任何线程进行调用,但也可以直接通过接口指针(不必编组)传递到任何它所创建的线程,并且这些线程可以通过这些指针进行调用。因此,如果客户将指向客户实现对象的接口指针(如水槽)传递给此对象,则可以选择通过该接口指针从任何它所创建的线程进行回调。如果客户是 STA,从一个与创建水槽对象的线程不同的线程进行直接调用将出现错误(如上面 2 中所示)。因此,COM 总是确保与 STA 相关的线程中的客户只能通过代理对这类进程内对象进行访问。此外,这些对象不应与自由线程编组程序进行集合,因为这将允许它们在 STA 线程上直接运行。 |
4. | 支持单元模型和自由线程的进程内服务器 - 标为 ThreadingModel=Both: 此服务器所提供的对象实现了其自身的同步化,并且可以由多个客户单元并行访问。此外,此对象不必通过代理而在 STA 或客户进程的 MTA 中被直接地创建和使用。由于此对象在 STA 中被直接使用,服务器必须对线程间对象(可能来自其它服务器)的接口进行编组以便按线程适当的方式对其它任何对象的访问可以有所保证。此外,通过将对象的线程模型标记成“两者”的方式,对象的实现程序如下所示:此对象可以从任何其它客户线程进行调用,但任何从此对象向客户的调用将只在对象接收了回调对象接口指针的单元上进行。COM 允许在 STA 和客户进程的 MTA 中直接创建这样一个对象。 由于创建这种对象的任何单元总是获得一个直接指针,而非代理指针,因此 ThreadingModel“两者”对象在 STA 中加载时,在 ThreadingModel“自由”对象基础上的对性能进行了改进。 由于 ThreadingModel “两者”对象也是针对 MTA 访问而设计(在内部它是完全线程安全),因此可以通过与由 CoCreateFreeThreadedMarshaler 提供的编组程序进行集合的方式而加速性能。该系统所提供的对象被集合进任何调用对象中,并将指向对象的自定义编组直接指针集合到进程中的所有单元中。然后,不论是 STA 还是 MTA,任何单元的客户不必通过代理可以直接访问对象。例如,STA 模型客户在 STA1 中创建进程内对象并将对象编组到 STA2 上。如果对象不与自由线程编组程序进行集合,则 STA2 获得通过代理访问对象的权限。如果与自由线程编组程序进行集合,则自由线程编组程序为 STA2 提供一个到对象的直接指针。 备注:与自由线程进行集合时一定要格外小心。例如,假设标有 ThreadingModel “两者”(同时与自由线程编组程序进行集合)的对象有一个数据成员,该数据成员为到其 ThreadingModel 是“单元”的另一个对象的接口指针。然后假设 STA 创建第一个对象,并且在创建过程中第一个对象创建了第二个对象。根据上述规则,第一个对象现在有一个到第二个对象的直接指针。现在,假设 STA 将到第一个对象的接口指针编组到另一个单元。由于第一个对象与自由线程编组程序进行了集合,所以到第一个对象的直接指针被给了第二个单元。如果第二个单元随后通过此指针进行调用,并且如果此调用引起第一个对象的调用是通过到第二个对象的接口指针来完成,则已经发生错误,因为第二个对象不应该被从第二个单元直接调用。如果第一个对象正有一个通过代理指向第二个对象的指针,而不是一个直接指针,则将导致其它类型的错误。系统代理也是一个只与一个单元相关的 COM 对象。为了避免某些迂回情况发生,它们跟踪其单元。所以,从与不同单元相关的代理上(而不是与该对象正在运行的线程上)调出对象,将接收到来自该代理的 RPC_E_WRONG_THREAD 返回,该调用将失败。 |
客户与进程内对象间的线程模型互操作性
客户和进程内对象间线程模型的互操作性的任何组合都是允许的。
COM 允许所有 STA 模型客户与单线程进程内对象进行相互操作,这可以通过创建并访问客户的主 STA 中的对象,并将其编组到调用 CoCreateInstance[Ex]的客户 STA 上来实现。
如果客户中的 MTA 在进程内服务器中创建了一个 STA 模型,则 COM 在客户中向上自旋一个“主机”STA。此主机 STA 创建对象,并且接口指针将被编组回 MTA。同样,当 STA 创建一个进程内 MTA 服务器时,COM 向上自旋一个主机 MTA,在其中创建对象并将其编组回 STA。由于单线程模型只是 STA 模型的一个退化情况,所以单线程模型与 MTA 模型之间的互操作性也进行了类似的处理。
对于任何进程内服务器实现的自定义接口,如果要支持那种要求 COM 对客户单元间的接口进行编组的互操作性能,则都需要提供编组代码。请参见本文下边的参考资源章节,以获取更多信息。
线程模型与返回的类集对象之间的关系
进程内服务器被“加载到”单元的明确定义在以下的两个步骤中进行解释:
1. | 如果包含进程内服务器类的 DLL 以前还没有通过操作系统加载程序进行加载(映射到进程地址空间),则该执行操作且 COM 获得 DLL 输出的 DLLGetClassObject 函数地址。如果 DLL 以前已经过与任何单元相关的线程进行加载,请跳过此步骤。 |
2. | COM 使用与“加载”单元相关的线程(或对于 MTA 情况,一个线程)来调用由寻找所需类的 CLSID 的 DLL 输出的 DllGetClassObject 函数。然后,返回的集对象用于创建该类对象的实例。 每当一个客户调用 CoGetClassObject/CoCreateIntance[Ex] 时,即使是从同一单元内,都会发生第二个步骤(由 COM 调用 DllGetClassObject)。换而言之,DllGetClassObject 可以由与同一单元相关的线程调用多次;这完全取决于在该单元中有多少个客户正在试图为该类获得一个类集对象的访问权限。 |
1. | DllGetClassObject 根据调用线程返回一个唯一类集对象(意味着一个 STA 对应一个类集对象,且潜在情况下在 MTA 内对应多个类集)。 |
2. | 不管调用线程的身份如何,或与调用线程相关的单元种类如何,DllGetClassObject 总是返回相同的类集。 |
3. | DllGetClassObject 根据调用单元返回一个唯一的类集对象(在 STA 和 MTA 中一个单元对应一个)。 |
进程内服务器的客户与对象线程模型总结
下表总结了客户线程在作为进程内服务器实现的类上第一次调用 CoGetClassObject 时,不同线程模型之间的相互作用。
客户/线程的类型:
• | 客户在一个与“主”STA 相关的线程(用 COINIT_APARTMENTTHREADED 标记调用 CoInitialize 或 CoInitializeEx 的第一个线程)中运行 — 这称为 STA0 (也称为单线程模型)。 |
• | 客户在一个与任何其它 STA [ASCII 150] 相关的线程中运行,称为 STA*。 |
• | 客户在一个与相关 MTA 的线程中运行。 |
• | 服务器没有 ThreadingModel 项 — 称为“无”。 |
• | 服务器标为“单元” — 称为“单元”。 |
• | 服务器标为“自由”。 |
• | 服务器标为“两者”。 |
客户 服务器 结果 ------ ------ ----------------------------------------- STA0 无 直接访问; 服务器加载到 STA0 STA* 无 代理访问; 服务器加载到 STA0。 MTA 无 代理访问; 服务器加载到 STA0; 如有必要,STA0 由 COM 自动创建; STA0 单元 直接访问; 服务器加载到 STA0 STA* 单元 直接访问; 服务器加载到 STA* MTA 单元 代理访问; 服务器加载到一个由 COM 自动创建的 STA。 STA0 自由 代理访问; 服务器加载到 MTA 如果必要 MTA 由 COM 自动创建。 STA* 自由 与 STA0->自由相同 MTA 自由 直接访问 STA0 两者 直接访问; 服务器加载到 STA0 STA* 两者 直接访问; 服务器加载到 STA* MTA 两者 直接访问; 服务器加载到 MTA