Есть ли способ обойти тупики блокировки загрузчика ОС, вызванные сторонними библиотеками? - PullRequest
4 голосов
/ 02 марта 2012

У меня есть интересная проблема, которую я не видел нигде документированной (по крайней мере, не эта конкретная проблема).

Эта проблема представляет собой сочетание 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, но тогда у меня нет никакого способа (который я знаю) поместить та же самая блокировка вокруг кода выхода потока (операционной системы).

Есть более очевидное решение этой проблемы, или мне просто не повезло?

Ответы [ 5 ]

2 голосов
/ 02 марта 2012

Пока все ваши модули работают в одном процессе, вы можете подключить Windows API, заменив некоторые системные вызовы вашими обертками.Затем можно обернуть вызовы в один критический раздел, чтобы избежать тупика.

Для достижения этой цели существует несколько библиотек и примеров. Этот метод обычно известен как обходной путь:

http://www.codeproject.com/Articles/30140/API-Hooking-with-MS-Detours

http://research.microsoft.com/en-us/projects/detours/

И, конечно, реализация оболочек должна выполняться в нативном коде, предпочтительно C ++.Обходы .NET также работают для высокоуровневых функций API, таких как MessageBox , но если вы попытаетесь переопределить LoadLibrary вызов API в .NET, то вы можете получить проблему циклической зависимости, потому что .NETсреда выполнения внутренне использует LoadLibrary во время выполнения и делает это часто.

Таким образом, решение выглядит для меня так: отдельный модуль .DLL, который загружается в самом начале вашего приложения.Модуль устраняет проблему взаимоблокировки, исправляя несколько вызовов VB и Windows API своими собственными упаковщиками.Все обертки делают одно: заключите вызов в критическую секцию и вызовите исходную функцию API для выполнения реальной работы.

1 голос
/ 15 сентября 2015

Я не вижу причин, по которым вы не можете загрузить дополнительный экземпляр элемента управления ActiveX в свой код запуска и просто зависнуть на ссылке. Presto, больше не возникает проблем с блокировкой загрузчика, поскольку среда выполнения VB6 никогда не выключается.

1 голос
/ 03 марта 2012

РЕДАКТИРОВАТЬ: в ретроспективе, я не думаю, что это будет работать.Проблема заключается в том, что тупик может возникнуть в любой момент, когда завершается поток Win32, и поскольку потоки Win32 не отображают потоки 1: 1 на потоки .NET, мы не можем (в пределах .NET) принудительно заставить потоки Win32 получить блокировку раньшевыход.В дополнение к возможности переключения потока .NET на другой поток ОС существуют предположительно потоки ОС, не связанные с каким-либо потоком .NET (сборкой мусора и т. П.), Которые могут запускаться и выходить случайным образом.

Единственное, о чем я могу думать, - это создать свой собственный объект блокировки и окружить код, который создает экземпляр COM-объекта в блоке блокировки .NET, но тогда у меня нет способа (что язнать о) поставить такую ​​же блокировку вокруг кода выхода потока (операционной системы).

Это звучит как многообещающий подход.Из этого я заключаю, что вы можете изменить код службы, и вы говорите, что каждый поток явно освобождает COM-объект перед выходом, поэтому, вероятно, вы можете запросить блокировку в этот момент, либо непосредственно перед явным освобождением COM-объекта, либо сразу после него.Секрет заключается в том, чтобы выбрать тип блокировки, которая неявно снимается после выхода из потока, удерживающего его, например, мьютекс Win32 .

Вероятно, объект мьютекса Win32 не станетпрекращено до тех пор, пока поток не завершит все вызовы DLL_THREAD_DETACH, хотя я не знаю, задокументировано ли это поведение.Я не знаком с блокировкой в ​​.NET, но я предполагаю, что они вряд ли подойдут, потому что даже если существует правильный тип блокировки, она, вероятно, будет считаться отмененной, как только поток достигнет концараздел управляемого кода, т. е. перед вызовами DLL_THREAD_DETACH.

Если объекты мьютекса Win32 не справляются (или если вы очень разумно предпочитаете не полагаться на недокументированное поведение), вам может потребоваться реализовать блокировку самостоятельно,Один из способов сделать это - использовать OpenThread, чтобы получить дескриптор текущего потока и сохранить его в объекте блокировки вместе с событием или подобным объектом.Если блокировка была заявлена, и вы хотите дождаться ее доступности, используйте WaitForMultipleObjects, чтобы дождаться, пока не будет сообщен дескриптор потока или событие.Если событие сигнализируется, это означает, что блокировка была явно снята, если дескриптор потока сигнализирует, что она была неявно освобождена выходом потока.Очевидно, что реализация этого включает в себя множество хитрых деталей (например: когда поток явно снимает блокировку, вы не можете закрыть дескриптор потока, потому что другой поток может ожидать его, поэтому вам придется закрыть его, когда блокировкавместо этого заявлено следующее) но разобраться с этим не должно быть слишком сложно.

0 голосов
/ 14 августа 2018

Я написал довольно сложный код с использованием VB6, VC6 около 20 лет назад, и мне нужно перенести его на visual studio.net.Я просто взял функции, как я их написал, вместе с заголовочными файлами, исправил все ошибки компиляции (которых было МНОГО), а затем попытался загрузить их.Получив «loaderlock closed», я затем решил повторить все файлы, начиная с тех, от которых зависело несколько других файлов, и затем пошёл вверх, и по ходу дела я включил только файлы заголовков, которые требовались для этого конкретного файла.Результат загружается сейчас просто отлично.больше нет замков с замком.урок для меня - не включать больше файлов заголовков в конкретный файл cpp, чем это абсолютно необходимо.надеюсь, это поможет

от очень счастливого туриста !!

Дэвид

0 голосов
/ 03 марта 2012

Поскольку я все еще изучаю свои варианты, я все еще хотел посмотреть, смогу ли я реализовать решение в чистом коде .NET без использования собственного кода, ради простоты. Я не уверен, является ли это решением для защиты от дурака, потому что я все еще пытаюсь выяснить, действительно ли оно дает мне взаимное исключение, в котором я нуждаюсь, или это выглядит просто так.

Любые мысли или комментарии приветствуются.

Соответствующая часть кода приведена ниже. Некоторые заметки:

  • Метод HandleRpcRequest вызывается из потока пула потоков при получении нового сообщения от удаленного клиента
  • Это запускает отдельный поток STA, чтобы он мог безопасно сделать COM-вызов
  • DbRequestProxy - это тонкий класс-оболочка для реального класса COM, который я использую
  • Я использовал ManualResetEvent (_safeForNewThread), чтобы обеспечить взаимное исключение. Основная идея заключается в том, что это событие остается не сигнализированным (блокирующим другие потоки), если какой-либо конкретный поток собирается завершить работу (и, следовательно, потенциально может прекратить выполнение VB6). Событие сигнализируется снова только после полного завершения текущего потока (после завершения вызова Join). Таким образом, несколько потоков обработчика запросов могут выполняться одновременно, если только существующий поток не завершает работу.

Пока что я считаю этот код правильным и гарантирует, что два потока больше не смогут взаимоблокироваться в коде инициализации / завершения во время выполнения VB6, в то же время позволяя им выполняться одновременно большую часть времени выполнения. , но я мог бы что-то здесь упустить.

public class ClientHandler {

    private static ManualResetEvent _safeForNewThread = new ManualResetEvent(true);

    private void HandleRpcRequest(string request)
    {

        Thread rpcThread = new Thread(delegate()
        {
            DbRequestProxy dbRequest = null;

            try
            {
                Thread.BeginThreadAffinity();

                string response = null;

                // Creates a COM object. The VB6 runtime initializes itself here.
                // Other threads can be executing here at the same time without fear
                // of a deadlock, because the VB6 runtime lock is re-entrant.

                dbRequest = new DbRequestProxy();

                // Call the COM object
                response = dbRequest.ProcessDBRequest(request);

                // Send response back to client
                _messenger.Send(Messages.RpcResponse(response), true);
                }
            catch (Exception ex)
            {
                _messenger.Send(Messages.Error(ex.ToString()));
            }
            finally
            {
                if (dbRequest != null)
                {
                    // Force release of COM objects and VB6 globals
                    // to prevent a different deadlock scenario with VB6
                    // and the .NET garbage collector/finalizer threads
                    dbRequest.Dispose();
                }

                // Other request threads cannot start right now, because
                // we're exiting this thread, which will detach the VB6 runtime
                // when the underlying native thread exits

                _safeForNewThread.Reset();
                Thread.EndThreadAffinity();
            }
        });

        // Make sure we can start a new thread (i.e. another thread
        // isn't in the middle of exiting...)

        _safeForNewThread.WaitOne();

        // Put the thread into an STA, start it up, and wait for
        // it to end. If other requests come in, they'll get picked
        // up by other thread-pool threads, so we won't usually be blocking anyone
        // by doing this (although we are blocking a thread-pool thread, so
        // hopefully we don't block for *too* long).

        rpcThread.SetApartmentState(ApartmentState.STA);
        rpcThread.Start();
        rpcThread.Join();

        // Since we've joined the thread, we know at this point
        // that any DLL_THREAD_DETACH notifications have been handled
        // and that the underlying native thread has completely terminated.
        // Hence, other threads can safely be started.

        _safeForNewThread.Set();

    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...