https://docs.microsoft.com/en-us/windows/desktop/com/processes--threads--and-apartments
进程、线程和套间
多线程应用程序必须避免两个线程问题,死锁,冲突。COM call control 可以帮助避免两个对象之间的调用时的死锁问题。COM 支持一些功能,这些功能被设计为,帮助进程外服务避免冲突的情况。
套间和COM 线程结构
将进程中所有的COM 对象分成组,这些组就称为套间。一个COM 对象存在于一个套间中,此时,它的方法可以被属于这个套间的线程合法的直接调用。其它任何线程如果想调用该对象的方法,必须经过代理。
有两种类型的套间:单线程套间,多线程套间。
1. 单线程套间包含一个线程,STA 中所有的COM 对象只能从属于STA 的线程接收到方法调用。STA 中所有COM 对象的方法调用,都将用STA 的线程的windows 消息队列的方式同步。
2. 多线程套间包含一个或多个线程,因此所有的MTA 中的所有COM 对象能从属于MTA 的任意一个线程直接接收到方法调用。MTA 中的线程使用了一个称作free-threading 的模型。调用MTA 中的对象的方法,将被对象本身同步。
一个进程可以拥有,0+STA,0或1MTA
进程中,第一个被初始化的STA 被称为主STA。套间之间的调用参数将被调整,COM 通过messaging处理它们的同步。如果设置一个进程中的多个线程为free-threaded,所有的free 线程存在于同一个套间,MTA 中任意线程的参数都是直接传递的,coder 必须处理所有的同步问题。一个同时拥有free-threading 和 apartment threading 的进程,所有的free threads 在一个单独的套间,所有其它的套间都是单线程套间。
https://docs.microsoft.com/en-us/windows/desktop/com/single-threaded-apartments
使用STA,提供了一种基于消息的模型,解决多对象同时运行的问题。它使得coder 可以编写更加高效的代码,以允许一个线程,在它等待一个耗时的操作完成的时候,允许另外一个线程执行。
初始化为套间模型进程中的每个线程,以及接收并分发window 消息的线程,都是一个STA 线程。每个线程都在其自己的套间中。套间内所有对象的通信、调用都是直接的,不需要转换。
逻辑上一组的相关的、执行在相同的线程的对象,因此必须同步执行,它们可以存在于一个STA 线程。但,一个套间模型对象,不能存在于多个线程。
对于其它进程中的对象的调用,必须在拥有该对象的进程的上下文中进行,因此,分发COM 将在你调用代理的时候自动的转换线程。
STA 规则:
1. 每个对象,只存在于一个线程
2. 为每个线程初始化COM library
3. 当在套间之间传递对象的指针时,需要转换
4. 每个STA 需要有一个message loop。STA 没有对象(客户),也需要一个message loop,以发送某些应用程序使用的广播信息。
5. DLL-based 或 进程内对象,不调用COM 初始函数;相应的,它们注册它们在注册表键的InprocServer32 子键下的ThreadingModel 值下注册它们的线程模型。可识别套间的对象,必须小心的编写DLL 入口点。
客户进程的每个线程或者进程外服务必须调用CoInitialize 或 调用CoInitializeEx 并在dwCoInit参数指定COINIT_APARTMENTTHREADED 。第一个调用CoInitializeEx 的线程为主套间。
对于一个对象的调用,必须在它自己的线程上(它的套间上)。禁止从另外的线程(这里讨论的是STA,因为,线程就是套间)来直接调用一个对象。COM 提供了两个函数来帮助达成这个目的:
1. CoMarshalInterThreadInterfaceInStream 转换一个接口为一个流对象,并将其返回给调用者
2. CoGetInterfaceAndReleaseStream 逆转换一个流对象,得到一个接口指针,之后会释放该流对象。
上面的函数,内部调用CoMarshalInterface 和 CoUnmarshalInterface 函数,并传入MSHCTX_INPROC标志。
通常来说,COM 将自动完成这个转换的过程,比如,将一个指针传递给代理或者调用CoCreateInstance。但有时,coder 使用非常规的方法在套间之间传递指针,此时需要正确的处理转换操作。
如果进程中的一个套间(A1)有一个接口指针,且另一个套间(A2)请求使用它。A1 必须调用CoMarshal* 来转换该接口。生成的流对象是线程安全的,它应该被存储在A2 绑定的一个变量上。A2 必须调用CoGetInterface* ( 流对象),以得一个指向了代理的指针,通过该代理,A2 可以访问该接口。在客户完成COM 工作之前,主套间需要一直alive。以这种方式在线程之前传递对象时,将接口作为参数传递将非常方便,分发COM 将为应用程序做转换和线程转换操作。
STA 必须有一个消息循环,如果使用了与其它线程的其它同步方式,使用MsgWaitForMultipleObjects 即可。
COM 为每个STA线程创建了一个windows class 为 "OleMainThreadWndClass"的隐藏窗口。对一个对象的调用,将被以对这个隐藏窗口的消息的形式接受。当一个对象的套间接受并分发消息,隐藏窗口将接收到它。windows 过程将调用对应的对象的接口方法。
多客户调用对象--->消息队列-->一次一个。自动同步。STA 可以实现IMessageFilter 以允许或禁止特定的windows 消息。
windows 窗口消息,是可以重入的,当它处理某个消息的时候,它接受并分发了消息,此时产生重入。STA类似。
https://docs.microsoft.com/en-us/windows/desktop/com/multithreaded-apartments
不使用windows 消息,MTA 中创建的对象需要有能力处理来自其它线程任意时间的访问。
可以有效的利用多线程的高性能又是,但接口实现时需要提供同步能力。另外,对象不控制访问它的线程的生命周期,不会在对象上存储线程特定的信息。
需要注意的点:
1. MTA 中making calls时,不会接收到函数调用(在同一个线程)
2. MTA 不会实现输入-同步 调用。
3. 异步调用,被转换为同步调用,在MTA 中
4. MTA 中,任何线程的消息过滤都不被调用
将线程初始化为free-threaded,调用CoInitializeEx(CONINIT_MULTITHREADED)
MTA 进程外服务,COM 通过RPC 子系统,在服务进程中创建一个线程池,客户调用,可以通过其中的任意线程在任意时间被提交。
当客户对进程外套间对象进行COM 调用,它将暂时suspend,之后,当call 返回,客户恢复执行。进程间的调用是由RPC 处理的。
https://docs.microsoft.com/en-us/windows/desktop/com/in-process-server-threading-issues
进程内服务线程的问题
进程内服务,不通过调用CoInitialize、CoInitializeEx 或 OleInitialize 来标志它的线程模型,需要在注册表中显式的指定其线程模型,如果不指定,默认的,线程模型为:每个进程一个单独的线程。
当一个客户的MTA 创建一个STA 进程内服务器,COM 创建一个STA “host” 线程。该host thread 将创建给对象,接口指针,将被marshaled back 到客户的MTA。 类似的,当套间-模型的客户的STA 创建一个MTA 进程内服务,COM 创建一个MTA host thread(对象将在此被创建,其指针被marshaled ,并传回给客户的STA ),
当进程内DLL ThreadingModel 设置为 Both,该DLL 创建的对象可以被创建,并被直接访问(STA或MTA)。但是,只能被它所存在的套间直接访问。为了将其指针传递给其它的套间,该对象必须被marshaled,该DLL 对象必须实现它自己的同步,并实现多线程访问能力。
为了提高MTA 对于 进程内DLL 对象的访问速度,COM 提供CoCreateFreeThreadedMarshaler 函数。该函数创建一个free-threaded marshaling 对象,该对象可以在请求进程内服务对象的时候合并使用。当同一个进程内的客户套间,需要访问另一个套间中的对象,合并的free-threaded marshaler 给客户提供了一个服务对象的直接的指针,而不是代理的指针。客户不需要做任何的同步操作。这个操作,仅限于同一个进程。
http://www.ecs.syr.edu/faculty/fawcett/handouts/CSE775/Presentations/Apartments.ppt
套间:
支持三种套间:
1. 单线程套间(STAs)
COM 通过windows 消息循环来序列化所有的外部到套件线程的调用。套间的单线程通过从消息队列中取出消息并执行的方式“服务”其它的方法调用
这意味着,不是线程安全的组件,可以安全的在win32 多线程环境下执行,只要它是被STA 创建的
2. 多线程套间MTA
COM 在MTA中没有条件序列化。任何在MTA 中创建的组件应该提供自己的同步能力,以保证线程安全。
3. 中立线程套间(NTA)
任何线程都可能离开STA 或 MTA 来访问NTA。NTAs 必须是完全的提供自己的同步能力。
套间规则
1. 一个进程可以有多个STAs,但只能有一个MTA
2. 每个COM 对象只能属于一个套间
3. 一个线程在某个时间点,只能执行一个套间。当一个线程进入了一个套间,COM 用套间ID 来标记它。
4. 对象只能在那些,执行在该对象所属的套间的线程中被直接的访问。
5. STA 中的对象,将只被创建该STA 的线程访问。因此,对象从来不能在多个STA 中同时访问。
创建套间
STA
当客户或基于EXE 的组件调用:
CoInitialize(NULL);
或
CoInitializeEx(NULL,CONINIT_APARTMENTTHREADED)
MTA
当客户或基于EXE 的组件调用
CoInitializeEx(NULL,CONINIT_MULTITHREADED)
第一次,新线程在同一进程中的后续调用会导致这些线程加入MTA。
加入套间
1. 在注册表中宣布没有线程模型的进程内组件被加载到客户端的主(第一)STA中(如果存在)。否则,COM 会为组件创建主STA。
2. 具有ThreadingModel=Apartment 注册表项的进程内组件被加载到实例化组件的任何客户端STA中。
3. 具有Threadingmodel=Free 注册表项的进程内组件将加载到客户端的MTA 中(如果存在)。否则,COM为组件创建主机MTA。
4. ThreadingModel=Both,加载到创建它的客户套间,STA或MTA
套间内部,以及套间之间的调用
1. 套间之内的调用是直接的---没有封装被调用
----STA中的实例只可以被该STA 中这一个线程直接访问
----一个进程内组件,客户的创建该组件的STA线程
----MTA 中的实例,可以被套间中的任意线程同时直接访问
2. 在另一个套间中,对组件的任何调用将被调整
----在远程机器上的进程间
---同一机器上的不同进程间
----同一进程间的两个套间间
进程内组件中线程模型的比较
调整 接口指针
- 套间之间,接口指针必须被转换
- 接口指针是套间相关的,它们仅可以被所属的套间中的线程使用
- 调用QueryInterface 时,COM 将服务端的所有的接口指针转换,并返回给客户,如果它们属于不同的套间。
- 如果一个服务是,进程内的, 且存在于一个STA 中,就不需要转换,有函数可以实现这个目的,看上面,当然也可以使用转换。
组件激活
对象是被服务控制管理器(SCM)激活的:
1. 进程内,将组件服务的DLL 加载到客户的地址空间
2. 本地,进程外,加载服务的exe,到它自己的本地的进程中
3. 远程,进程外,通知远程机器的SCM 去激活服务的EXE,在它的机器上
4. 进程内DLL ,可以以本地或远程组件的方式加载,远程加载时,使用dllhost.exe 进程,作为宿主进程。
5. 对于所有进程外组件,COM 在客户的地址空间加载proxy.dll ,在组件的地址空间加载stub
SCM
SCM 支持三种激活过程:
1. 绑定到类对象(class factory),使用CoGetClassObject
2. 绑定到类实例,使用CoCreateInstanceEx
3. 绑定到文件中的永久实例,使用CoGetInstanceFromFile
绑定到类对象和实例,导致新创建的对象没有状态历史,除非,class 和 class factory 构造函数做一些工作。
创建单件组件,使所有的客户激活都绑定到同一个类实例是可能的
如果你需要在激活的时候,保留类状态,在必要的时候,可以使用永久绑定
MTA 组件 中的成员调用
当CoInitialized,COM 开启RPC 服务,使得组件称为一个RPC 服务器
1. 因为对象被主机客户进程访问,访问被注册的网络协议,一个RPC 线程缓存被开启。
2. 缓存中的第一个线程监听进来的连接,并分发线程以处理每一个请求。
3. 被分配的线程找到stub 管理器,和接口stub。
4. 线程进入组件的套间,并调用IPrcStubBuffer::Invoke 方法(接口的存根),并进入组件的方法
5. MTA 中,随后的线程可能同时访问该对象,因此,同步对全局和本地静态数据的访问十分重要
STA 组件成员调用
当CoInitialized,COM 开启RPC 服务,使得组件称为一个RPC 服务器
1. 因为对象被主机客户进程访问,访问被注册的网络协议,一个RPC 线程缓存被开启。
2. 缓存中的第一个线程监听进来的连接,并分发线程以处理每一个请求。
3. 没有线程可以进入到该STA,除了第一个调用CoInitialize 的线程,RPC 线程向STA 线程的消息队列,post 一个消息
4. STA 线程的消息循环处理队列(GetMessage、DispatchMessage)
5. 因为所有的调用都在STA 线程上,所有的调用被以windows 消息队列的方式同步了。
转换结构: