Com Threading / Apartment поведение несовместимо с упорядоченной фабрикой

Я пытаюсь использовать CoRegisterClassObject, чтобы настроить способ загрузки DLL, в которых есть объекты com. Я пробую что-то, что решит проблему, с которой я столкнулся, когда тип квартиры потока не соответствовал объекту 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, и тогда объекты будут жить там.

Проблема, которую я вижу, заключается в том, что в приведенном выше примере идентификаторы потоков не всегда выглядят так, как я ожидал. Если FactoryThread инициализирован как Многопоточный, а основной поток как многопоточный, то THREAD_ID_2 == THREAD_ID_3 == THREAD_ID_4! = THREAD_ID_1, как и ожидалось (фабрика создает эти объекты, и они могут жить в потоке фабрики). Если эти модели потоковой передачи переключаются, тогда thread_id_3 == thread_id_4, но они отличаются от thread_id_2 и thread_id_1, даже если объекты com могут быть созданы в потоке 2.

Это кажется непоследовательным и может вызвать нежелательное поведение в ситуациях, когда задействован другой поток. Если полагаться только на реестр и не использовать coregisterclassobject, если я создал объект со свободным потоком в STA, объект будет создан в другом потоке, порожденном com, который был в MTA, а затем, если я создал третий поток, который также был в STA, создание там объекта поместило бы его в первый поток MTA, порожденный com, а не новый (то же самое было бы, если бы модель потока объекта и типы апартаментов потоков были поменяны местами). Однако, если бы я использовал coregisterclassobject для создания моей собственной фабрики, как указано выше, и объект был многопоточным, но поток находился в STA, то каждый новый поток, создавший эти многопоточные объекты, порождал бы новый поток MTA, что кажется расточительным и непоследовательным. с тем, что обычно бывает.


person bdwain    schedule 24.07.2013    source источник


Ответы (1)


Когда вы создаете свою фабрику классов в многопоточном подразделении, ее можно вызывать из нескольких потоков. Отсюда и название «многопоточный». Почему именно вас это удивляет?

В частности, среда выполнения COM поддерживает пул потоков, которые выполняют вызовы между подразделениями в MTA. Любой объект, который объявляет себя многопоточным, может быть вызван в любом из этих потоков.

а затем, если я создал третий поток, который также был в STA, создание там объекта поместило бы его в первый поток MTA, порожденный com, а не новый

В этом утверждении нет особого смысла. Многопоточные объекты не принадлежат какому-либо конкретному потоку, поэтому непонятно, что вы имеете в виду под «объектом ... помещаем ... в поток MTA». Многопоточный объект может быть создан и вызван любым потоком, который присоединился к MTA (будь то поток, созданный вашей программой явно, или поток, созданный средой выполнения COM); он может вызываться несколькими такими потоками одновременно.

Разница в поведении, которую вы наблюдаете, объясняется этим фактом. Вызовы между подразделениями доставляются в потоки STA в виде оконных сообщений. Поток STA сигнализирует о своей готовности принимать входящие вызовы, вызывая GetMessage. С другой стороны, межсетевые вызовы в MTA не используют оконные сообщения, а используют какой-то другой, недокументированный и неуказанный механизм. Такой вызов может обслуживаться только потоком из пула потоков, созданного COM - среда выполнения COM не может просто реквентировать поток, который вы явно создали, поскольку она не знает, что этот поток делает в любой момент времени. Не существует API, который позволял бы вашему потоку сказать: «Я готов принимать и выполнять произвольные вызовы COM» - фактически присоединиться к пулу потоков COM.

Имея это в виду, давайте рассмотрим ваши сценарии. Случай A: у вас есть обычный COM-объект, зарегистрированный в ThreadingModel=Free, никакого забавного дела с фабрикой пользовательских классов. Поток STA вызывает CoCreateInstance с CLSID этого объекта. COM считывает информацию из реестра, обнаруживает, что объект является многопоточным, и маршалирует вызов одного из потоков в своем пуле потоков MTA, который создает объект и обратно маршалирует его указатель на интерфейс. Если поток STA (тот же самый или другой) снова вызывает CoCreateInstance с тем же CLSID, процесс повторяется, и может случиться так, что тот же поток из пула обработает его.

Кстати, поток, создавший объект, не обязательно должен быть тем же потоком, который обрабатывает вызов OutputOwningThreadId. Фактически, если вы вызываете OutputOwningThreadId дважды подряд - и особенно если вы вызываете его одновременно для одного и того же объекта из нескольких потоков - высока вероятность того, что он сообщит о разных идентификаторах потока. Это неправильное название: в MTA нет такой вещи, как «поток-владелец».

Случай B: вы запускаете свой явный FactoryThread, который создает фабрику классов, а затем занимаетесь чем-то чем-то (тот факт, что он запускает насос сообщений, не имеет значения в MTA; он также может быть Sleep(INFINITE)). Этот поток запрещен для среды выполнения COM; как я уже сказал, COM не может волшебным образом прервать его в середине того, что он делает, и заставить его выполнить некоторый вызов COM. Таким образом, как и в случае A, все последующие вызовы CreateInstance и (плохо названные) OutputOwningThreadId выполняются в некоторых потоках из пула потоков, поддерживаемых COM, но никогда не FactoryThread.

Да, в вашем подходе вы в основном тратите один поток. Это не похоже на огромную цену за возможность избежать регистрации в реестре.

person Igor Tandetnik    schedule 24.07.2013
comment
Понятно. Я не понимал, что COM по-разному взаимодействует с потоками STA и MTA. Спасибо за вашу помощь. - person bdwain; 25.07.2013