COM 线程/单元行为与编组工厂不一致

Com Threading/Apartment behavior inconsistent with a marshalled factory

本文关键字:工厂 不一致 线程 单元 COM      更新时间:2023-10-16

我正在尝试使用CoRegisterClassObject来自定义加载包含com对象的dll的方式。我正在尝试一些方法,以解决线程的单元类型与 com 对象的单元类型不匹配时遇到的问题。基本思想是,由于使用 coregisterclassobject 在创建 com 对象时会忽略注册表,因此我需要确保在 STA 线程中创建 STA 对象,对于 MTA 对象也是如此。这是我作为概念证明编写的示例,它并不总是像我预期的那样运行。

LPSTREAM factory_stream = NULL; //GLOBAL VARIABLE FOR TEST
DWORD __stdcall FactoryThread(LPVOID param)
{
   CoInitialize(NULL);
   //CoInitializeEx(NULL, COINIT_MULTITHREADED);
   cout << GetCurrentThreadId(); //THREAD_ID_2
   CustomClassFactory *factory = new CustomClassFactory();
   factory->AddRef();
   CoMarshalInterThreadInterfaceInStream(IID_IClassFactory, (IClassFactory*)factory, &factory_stream);
   MSG msg;
   while (GetMessage(&msg, NULL, 0, 0)) 
   {
     TranslateMessage(&msg);
     DispatchMessage(&msg);
   }
   factory->Release();
   CoUninitialize();
   return 0;
}

这是我主要功能的相关部分。

//CoInitialize(NULL);
CoInitializeEx(NULL, COINIT_MULTITHREADED);
cout << GetCurrentThreadId(); //THREAD_ID_1
HANDLE regThread = CreateThread(NULL, 0, FactoryThread, NULL, 0, NULL);
Sleep(5000); //ensures that the factory is registered
IClassFactory *factory = NULL;
CoGetInterfaceAndReleaseStream(factory_stream, IID_IClassFactory, (void**)&factory);
DWORD regNum = 0;
HRESULT res = CoRegisterClassObject(clsid, factory, CLSCTX_INPROC_SERVER, REGCLS_MULTI_SEPARATE, &regNum);
{
   TestComObjLib::ITestComObjPtr ptr;
   HRESULT hr = ptr.CreateInstance(__uuidof(TestComObjLib::TestComObjCoClass), NULL);
   ptr->OutputOwningThreadId(); //THREAD_ID_3 is just from cout << GetCurrentThreadId()
   TestComObjLib::ITestComObjPtr ptr2;
   HRESULT hr = ptr2.CreateInstance(__uuidof(TestComObjLib::TestComObjCoClass), NULL);
   ptr2->OutputOwningThreadId(); //THREAD_ID_4
}
CoRevokeClassObject(regNum);
CoUninitialize();

这个想法是,由于注册表不应该与 CoRegisterClassObject 一起使用,我需要在 STA 而不是当前的 MTA 线程中手动创建单元线程对象,反之亦然。我注意到,当不使用CoRegisterClassObject时,CoGetClassObject会生成一个新线程并在该线程中调用DllGetClassObject,因此我认为只需要在STA中创建类工厂,然后对象将驻留在那里。

我看到的问题是,在上面的示例中,线程 ID 并不总是看起来像我期望的那样。如果 FactoryThread 初始化为 Apartment 线程,主线程初始化为多线程,则 THREAD_ID_2 == THREAD_ID_3 == THREAD_ID_4 != THREAD_ID_1 (工厂正在创建这些对象,它们可以存在于工厂的线程中(。如果切换了这些线程模型,则thread_id_3 == thread_id_4,但它们与 thread_id_2 和 thread_id_1 不同,即使可以在线程 2 中创建 com 对象也是如此。

这似乎不一致,并且在涉及另一个线程的情况下可能会导致不必要的行为。当我只依赖注册表而不使用 coregisterclassobject 时,如果我在 STA 中创建了一个自由线程对象,则该对象将在 MTA 中的 com 生成的不同线程中创建,然后如果我生成了也在 STA 中的第三个线程,在那里创建对象会将其放在第一个 com-surwed MTA 线程中, 不是一个新的(如果对象的线程模型和线程的单元类型颠倒,则也是如此(。但是,如果我使用 coregisterclassobject 像上面一样创建我自己的工厂,并且该对象是多线程的,但线程在 STA 中,那么创建这些多线程对象的每个新线程都会生成一个新的 MTA 线程,这似乎是浪费并且与通常发生的情况不一致。

在多线程单元中创建类工厂时,可以从多个线程调用它。因此得名"多线程"。你为什么觉得它令人惊讶?

具体而言,COM 运行时维护一个线程池,这些线程池在 MTA 中执行跨单元调用。然后,可以在任何这些线程上调用声明自己为多线程的任何对象。

然后,如果我生成了也在 STA 中的第三个线程,则创建 那里的对象会把它放在第一个 MTA 线程中,而不是 一个新的

这种说法没有多大意义。多线程对象不属于任何特定线程,因此不清楚"对象...放。。。在 MTA 线程中"。可以创建并调用加入 MTA 的任何线程(无论是程序显式创建的线程,还是由 COM 运行时创建的线程(;它可以由多个这样的线程同时调用。

你观察到的行为差异是由于这个事实。跨单元调用以窗口消息的形式传递到 STA 线程。STA 线程通过调用 GetMessage 来表示其已准备好接受传入呼叫。另一方面,对 MTA 的跨单元调用不使用窗口消息,而是使用其他一些未记录和未指定的机制。此类调用只能由 COM 创建的线程池中的线程提供服务 - COM 运行时不能只命令显式创建的线程,因为它不知道该线程在任何给定时间正在执行的操作。没有 API 允许线程说"我已准备好接受并执行任意 COM 调用" - 实际上加入 COM 的线程池。

考虑到这一点,让我们看一下你的方案。案例A:你有一个在ThreadingModel=Free注册的常规COM对象,没有自定义类工厂的有趣业务。STA 线程使用该对象的CLSID调用CoCreateInstance。COM 从注册表中读取信息,发现对象是多线程的,并封送对其 MTA 线程池中的一个线程的调用,该线程池将创建对象并将其接口指针封送回来。如果 STA 线程(同一线程或另一个线程(使用相同的CLSID再次调用CoCreateInstance,则重复该过程,并且可能会碰巧池中的同一线程处理它。

顺便说一下,创建对象的线程不必是处理OutputOwningThreadId调用的同一线程。事实上,如果您连续调用OutputOwningThreadId两次 - 特别是如果您从多个线程同时调用同一对象 - 它很有可能会报告不同的线程 ID。这是一个用词不当:在MTA中,没有"拥有线程"这样的东西。

情况 B:你旋转显式FactoryThread,这会创建类工厂,然后忙于做一些事情(它旋转消息泵的事实在 MTA 中无关紧要;它也可以Sleep(INFINITE)(。此线程禁止 COM 运行时使用;正如我所说,COM 不能在它正在做的任何事情中神奇地中断它,并让它执行一些 COM 调用。因此,就像在案例 A 中一样,所有后续CreateInstance和(名称不正确的(OutputOwningThreadId调用都在 COM 维护的线程池中的某些线程上执行,但永远不会在 FactoryThread 上执行。

是的,在你的方法中,你基本上是在浪费一个线程。对于能够避免注册表的好处来说,这似乎并不是一个巨大的代价。