我有一个有趣的问题,我在其他任何地方都看不到(至少不是这个特定问题)。

此问题是COM,VB6和.NET的组合,使它们的播放效果很好。

这是我所拥有的:

  • 旧版VB6 ActiveX DLL(由我们编写)
  • 用C#编写的多线程Windows服务,该服务通过网络处理来自客户端的请求并发送回结果。它通过创建一个新的STA线程来处理每个请求来做到这一点。每个请求处理程序线程实例化一个COM对象(在ActiveX DLL中定义)以处理请求并获取结果(传入XML字符串,并返回XML字符串),显式释放COM对象,并退出。然后,服务将结果发送回客户端。
  • 所有网络代码均使用异步联网(即线程池线程)进行处理。

  • 是的,我知道首先要做的就是冒险,因为VB6对多线程应用程序一开始并不十分友好,但是不幸的是,这是我目前所坚持的。

    我已经修复了许多导致代码死锁的问题(例如,确保实际上创建了COM对象并从单独的STA线程调用了这些对象,确保在线程退出之前明确释放了COM对象,以防止垃圾回收器和COM Interop代码之间发生了死锁等),但是有一种死锁场景我似乎无法解决。

    在WinDbg的一些帮助下,我能够弄清楚正在发生什么,但是我不确定如何(或是否)可以解决这种特殊的僵局。

    发生了什么事

    如果一个请求处理程序线程正在退出,而另一个请求处理程序线程正在同时启动,则可能会由于VB6运行时初始化和终止例程的工作方式而发生死锁。

    在以下情况下会发生死锁:
  • 正在启动的新线程正在创建(VB6)COM对象的新实例以处理传入请求。此时,COM运行时处于检索对象的类工厂的调用中间。类工厂的实现在VB6运行时本身中( MSVBVM60.dll )。也就是说,它调用了VB6运行时的 DllGetClassObject 函数。依次调用内部运行时函数(MSVBVM60!CThreadPool::InitRuntime),该函数获取互斥量并进入关键部分以完成其部分工作。此时,即将调用 LoadLibrary 来将 oleaut32.dll 加载到进程中,同时保持此互斥量。因此,现在它持有此内部VB6运行时互斥对象并等待OS加载程序锁定。
  • 退出的线程已经在加载程序锁内运行,因为它已完成执行托管代码,并且正在 KERNEL32!ExitThread 函数内执行。具体来说,它正在处理该线程上的 MSVBVM60.dll DLL_THREAD_DETECH消息,这又调用了一种方法来终止线程上的VB6运行时(MSVBVM60!CThreadPool::TerminateRuntime)。现在,该线程尝试获取与要初始化的其他线程已经具有的互斥体。

  • 经典的僵局。线程A具有L1并需要L2,但是线程B具有L2并需要L1。

    问题(如果您到目前为止已经关注了我)是我无法控制VB6运行时在其内部线程初始化和拆卸例程中的工作。

    从理论上讲,如果我可以强制VB6运行时初始化代码在OS加载程序锁中运行,则可以避免死锁,因为我可以肯定VB6运行时所持有的互斥锁仅在初始化和终止例程中使用。

    要求
  • 我无法从单个STA线程进行COM调用,因为这样该服务将无法处理并发请求。我也不能长时间运行请求来阻止其他客户端请求。这就是为什么我为每个请求创建一个STA线程的原因。
  • 我需要在每个线程上创建一个COM对象的新实例,因为我需要确保每个实例在VB6代码中具有自己的全局变量副本(VB6为每个线程提供其自己的所有全局变量副本)。

  • 我尝试过的解决方案无效

    将ActiveX DLL转换为ActiveX EXE

    首先,我尝试了一种显而易见的解决方案,并创建了一个ActiveX EXE(进程外服务器)来处理COM调用。最初,我对其进行编译,以便为每个传入请求创建一个新的ActiveX EXE(进程),并且还使用每个对象的线程编译选项进行了尝试(创建了一个进程实例,并在一个新的实例上创建了每个对象。 ActiveX EXE中的线程)。

    这解决了有关VB6运行时的死锁问题,因为VB6运行时从不加载到适当的.NET代码中。但是,这导致了另一个问题:如果并发请求进入服务,则ActiveX EXE会随机失败,并出现RPC_E_SERVERFAULT错误。我认为这是因为COM编码和/或VB6运行时无法在ActiveX EXE中处理并发对象创建/销毁或并发方法调用。

    强制VB6代码在OS加载程序锁中运行

    接下来,我切换回对COM类使用ActiveX DLL。为了强制VB6运行时在OS加载程序锁内运行其线程初始化代码,我创建了 native (Win32)C++ DLL,其中的代码用于处理 DllMain 中的DLL_THREAD_ATTACHDLL_THREAD_ATTACH代码调用 CoInitialize ,然后实例化虚拟VB6类以强制加载VB6运行时,并强制运行时初始化例程在线程上运行。

    Windows服务启动时,我使用 LoadLibrary 将此C++ DLL加载到内存中,以便该服务创建的任何线程都将执行该DLL的DLL_THREAD_ATTACH代码。

    问题是该代码在服务创建的每个线程上运行,包括.NET垃圾收集器线程和异步网络代码使用的线程池线程,它们运行不佳(这似乎导致线程永远无法运行)。正常启动,我想在GC和线程池线程上初始化COM通常是一个非常糟糕的主意)。



    是否可以解决这些问题?

    所以,我的问题是,有什么办法可以解决原始的死锁问题?

    我唯一想到的另一件事是创建自己的锁对象,并在.NET lock块中包含实例化COM对象的代码,但是我(我知道)无法在该对象周围放置相同的锁。 (操作系统的)线程退出代码。

    这个问题是否有更明显的解决方案,还是我在这里很不幸?

    最佳答案

    只要所有模块都在一个进程中工作,就可以通过用包装器替换某些系统调用来挂接Windows API。然后,您可以将调用包装在单个关键部分中,以避免死锁。

    有几个库和示例可以实现此目的,该技术通常称为绕行:

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

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

    当然,包装程序的实现应使用 native 代码(最好是C++)完成。 .NET绕道也可以用于MessageBox等高级API函数,但是如果尝试在.NET中重新实现LoadLibrary API调用,则可能会遇到循环依赖性问题,因为.NET运行时在执行过程中内部使用LoadLibrary函数,并且经常这样做。

    因此,解决方案在我看来是这样的:一个单独的.DLL模块在您的应用程序开始时加载。该模块通过使用自己的包装器修补几个VB和Windows API调用来解决死锁问题。所有包装器都做一件事:将调用包装在关键部分中,并调用原始API函数来完成实际工作。

    关于c# - 有什么方法可以解决由第三方库引起的OS加载程序锁死锁?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/9528100/

    10-13 06:54