У меня есть интересная проблема, которую я не видел нигде документированной (по крайней мере, не эта конкретная проблема).
Эта проблема представляет собой сочетание COM, VB6 и .NET и заставляет их играть хорошо.
Вот что у меня есть:
- Устаревшая библиотека ActiveX VB6 (написанная нами)
- Многопоточная служба Windows, написанная на C #, которая обрабатывает запросы отклиенты по сети и отправляет обратно результаты.Это делается путем создания нового потока STA для обработки каждого запроса.Каждый поток-обработчик запросов создает экземпляр COM-объекта (определенного в ActiveX DLL) для обработки запроса и получения результата (передается строка XML и возвращается строка XML), явно освобождает COM-объект ивыходы.Затем служба отправляет результат обратно клиенту.
- Весь сетевой код обрабатывается с использованием асинхронной сети (т. Е. Потоков пула потоков).
И да, я знаю, что этово-первых, это уже рискованно, поскольку VB6 не очень дружит с многопоточными приложениями, но, к сожалению, сейчас я застрял на этом.
Я ужеисправлен ряд вещей, которые вызывали взаимоблокировки в коде (например, удостоверившись, что объекты COM фактически созданы и вызваны из отдельного потока STA, убедившись в явном освобождении объектов COM до выхода из потока, чтобы предотвратить возникшие взаимоблокировкимежду сборщиком мусора и кодом COM-взаимодействия и т. д.), но есть один сценарий тупика, который я просто не могу решить.
С некоторой помощью WinDbg мне удалось выяснить что происходит, но я не уверен, как (или если) существует способ обойти этот конкретный тупикk.
Что происходит
Если один поток обработчика запросов завершается, и одновременно запускается другой поток обработчика запросов, из-заспособ, которым работают подпрограммы инициализации и завершения VB6.
В следующем сценарии возникает тупик:
Новый поток, который запускается, находится посерединесоздания нового экземпляра COM-объекта (VB6) для обработки входящего запроса.На этом этапе среда выполнения COM находится в середине вызова для получения фабрики классов объекта.Реализация фабрики классов находится в самой среде выполнения VB6 ( MSVBVM60.dll ).То есть вызывается функция DllGetClassObject среды выполнения VB6.Это, в свою очередь, вызывает внутреннюю функцию времени выполнения (MSVBVM60!CThreadPool::InitRuntime
), которая получает мьютекс и входит в критическую секцию для выполнения части своей работы.В этот момент он собирается вызвать LoadLibrary , чтобы загрузить oleaut32.dll в процесс, удерживая этот мьютекс.Итак, теперь он удерживает этот внутренний мьютекс времени выполнения VB6 и ожидает блокировки загрузчика ОС.
Выходящий поток уже работает внутри блокировки загрузчика, поскольку он завершил выполнение управляемого кода ивыполняется внутри функции KERNEL32! ExitThread .В частности, он находится в середине обработки сообщения DLL_THREAD_DETECH
для MSVBVM60.dll в этом потоке, которое, в свою очередь, вызывает метод завершения среды выполнения VB6 в потоке (MSVBVM60!CThreadPool::TerminateRuntime
).Теперь этот поток пытается получить тот же мьютекс, который уже есть у другого инициализируемого потока.
Классический тупик.Поток A имеет L1 и хочет L2, но поток B имеет L2 и нуждается в L1.
Проблема (если вы следовали за мной так далеко) заключается в том, что у меня нет никакого контроля над тем, что делает среда выполнения VB6в его процедурах инициализации и разрыва внутреннего потока.
Теоретически, если бы я мог заставить код инициализации среды выполнения VB6 запускать внутри блокировки загрузчика ОС, я бы предотвратил тупик, потому что яДостоверно уверен, что мьютекс, который содержит среда выполнения VB6, специально используется только в процедурах инициализации и завершения.
Требования
Я не могу сделать вызовы COM из одного потока STA, потому что тогда служба не сможет обрабатывать параллельные запросы. У меня не может быть долго выполняющегося запроса, блокирующего другие клиентские запросы. Вот почему я создаю один поток STA для каждого запроса.
Мне нужно создать новый экземпляр объекта COM в каждом потоке, потому что мне нужно убедиться, что каждый экземпляр имеет свою собственную копию глобальных переменных в коде VB6 (VB6 дает каждому потоку свою собственную копию всех глобальные переменные).
Решения, которые я пробовал, которые не работали
Преобразованная библиотека ActiveX DLL в ActiveX EXE
Сначала я попробовал очевидное решение и создал ActiveX EXE (внепроцессный сервер) для обработки вызовов COM. Первоначально я скомпилировал его так, чтобы новый ActiveX EXE (процесс) создавался для каждого входящего запроса, и я также пробовал его с опцией компиляции Thread Per Object * (создается один экземпляр процесса, и он создает каждый объект в новом потоке в ActiveX EXE).
Это устраняет проблему взаимоблокировки в отношении среды выполнения VB6, поскольку среда выполнения VB6 никогда не загружается в соответствующий код .NET. Однако это привело к другой проблеме: если в службу поступают параллельные запросы, EXE-файл ActiveX имеет тенденцию случайного сбоя с ошибками RPC_E_SERVERFAULT
. Я предполагаю, что это потому, что маршаллинг COM и / или среда выполнения VB6 не могут иметь дело с одновременным созданием / уничтожением объекта или одновременными вызовами методов внутри ActiveX EXE.
Принудительно запустить код VB6 внутри блокировки загрузчика ОС
Затем я вернулся к использованию ActiveX DLL для класса COM. Чтобы заставить среду выполнения VB6 запускать свой код инициализации потока внутри блокировки загрузчика ОС, я создал собственную (Win32) C ++ DLL с кодом для обработки DLL_THREAD_ATTACH
в DllMain . Код DLL_THREAD_ATTACH
вызывает CoInitialize , а затем создает фиктивный класс VB6, чтобы принудительно загрузить среду выполнения VB6 и запустить подпрограмму инициализации среды выполнения в потоке.
Когда служба Windows запускается, я использую LoadLibrary , чтобы загрузить эту C ++ DLL в память, чтобы любые потоки, созданные службой, выполняли код этой библиотеки DLL_THREAD_ATTACH
.
Проблема в том, что этот код выполняется для каждого потока, создаваемого службой, включая поток сборщика мусора .NET и потоки пула потоков, используемые асинхронным сетевым кодом, который плохо заканчивается ( кажется, это просто приводит к тому, что потоки никогда не запускаются должным образом, и я предполагаю, что инициализация COM в GC и потоках пула потоков - это вообще очень плохая идея).
Добавление
Я только что понял, почему это плохая идея (и, вероятно, отчасти она не сработала): небезопасно вызывать LoadLibrary , когда вы держите блокировку загрузчика. См. Примечания раздел в этой статье MSDN: http://msdn.microsoft.com/en-us/library/ms682583%28VS.85%29.aspx,, в частности:
Потоки в DllMain удерживают блокировку загрузчика, поэтому никакие дополнительные DLL не могут быть динамически загружены или инициализированы.
Есть ли способ обойти эти проблемы?
Итак, мой вопрос, есть ли какой-либо способ обойти исходную проблему взаимоблокировки?
Единственное, о чем я могу думать, - это создать свой собственный объект блокировки и окружить код, который создает экземпляр COM-объекта в блоке .NET lock
, но тогда у меня нет никакого способа (который я знаю) поместить та же самая блокировка вокруг кода выхода потока (операционной системы).
Есть более очевидное решение этой проблемы, или мне просто не повезло?