4

我正在使用管道将遗留应用程序(用 COBOL 编写)与我们的 .NET 新应用程序连接起来。这个想法很简单:遗留程序(我的 ERP 菜单)在流上写入一些参数,.NET 应用程序通过Console.In流读取它并启动一个新线程,打开请求的屏幕。这是 .NET 方面的一个片段,说明了这个想法是如何工作的:

<STAThread(), LoaderOptimization(LoaderOptimization.MultiDomain)>
Shared Sub Main()
    If Environment.GetCommandLineArgs(1) = "PIPE"
       While True
          Dim pipeParam as String
          Try
             pipeParam = Console.In.ReadLine()
          Catch e as Exception
             Exit Sub
          End Try

          ' Deal with parameters here

          If pipeParam = "END"
             Dim newThread as New Threading.Thread(Sub()
                                                       ' Create a new AppDomain and Loads the menu option, generally a Winforms form.
                                                   End Sub)
             newThread.Start()                 
          End If
       End While

    End If
End Sub

一切都很好,很容易......直到今天。我在我的客户端环境(Windows Server 2003)中部署了这个解决方案,并且碰巧没有执行任何请求的线程,除非被调用的进程(COBOL)被终止(Console.In即被强制关闭)。从那时起,所有请求的 winform 都将开始显示并按预期运行。

用日志挖掘这种奇怪的行为,我发现线程正常执行,直到执行IDictionary.ContainsKey()语句(或其他需要本机代码执行的方法)。此时,线程冻结/休眠。

如果我将线程创建限制为三个,那么每个创建的线程都会挂起,直到第三个线程,即Console.In.ReadLine不再执行的时候。

我应该尝试什么?有什么建议吗?

更多信息:到目前为止,我发现的最接近的方向是 Hans Passant 在这个问题中的回答: Interface freezes in multi-threaded c# application(.NET SystemEvents 恰好出现在我的调试器线程列表中,但我无法解决我对建议的解决方案的问题)。

更新新闻

我可以通过等待子线程完成加载Form. 这个“准备就绪”信号通过AppDomain.SetData()和传递AppDomain.GetData()。不知何故,在创建表单后,当主线程继续时,子线程不再冻结Console.ReadLine。虽然问题解决了,但我对此很感兴趣。我试图在“尽可能简单”的测试用例中重现这一点。

更多细节

  • 入口点 .exe 编译为 32 位。所有其他库都是“AnyCpu”。问题发生在 32 位(我的客户端)和 64 位(我的开发)机器(都是 windows Server 2003)上。
  • 更新Sub Main()了上述代码段中的属性。
  • 试图将其放入Console.ReadLine工作线程中。没有解决(见下图)。
  • COBOL 应用程序不会冻结,因为它是在单独的 OS 进程中执行的。管道恰好是我的 IPC 方法。在这种情况下,COBOL 应用程序只写入参数,不等待响应。
  • 堆栈跟踪如下图所示(线程PRX001235在连接到数据库之前和有效加载表单之前反序列化 xml 配置文件 - 在这种情况下,似乎仍在托管代码中 - 有时PRX001235线程会在本机代码中冻结试图连接到数据库):

线程与 StackTrace 的对比

4

3 回答 3

4

首先,你正在做一些非常不寻常的事情,所以不寻常的结果并不出人意料。其次,您所做的事情在 Windows SDK 文档中是严格禁止的。这表明创建单线程单元的线程永远不允许进行阻塞调用。

显然,您的工作线程在操作系统内部某处的某个锁上被阻塞,该锁由您的程序的主线程占用。查看其中一个被阻塞线程的调用堆栈以识别可能的锁会很有帮助。这需要启用非托管代码调试,以便您可以查看非托管堆栈帧并启用 Microsoft 符号服务器,以便您获得 Windows 代码的调试符号,并且可以从堆栈跟踪中理解。

没有一个可以看,我将不得不推测:

  • Console.ReadLine() 本身采用内部锁,使控制台可以安全地从多个线程中使用,从而对 Read() 的调用进行序列化。任何使用 Console 方法的线程都可以命中同一个锁。这不太可能是罪魁祸首,这些工作线程不太可能也在使用控制台。

  • 严格禁止的角度与单元线程的相关性有关,这是一个 COM 功能。STA 由程序的 Main() 方法上的 [STAThread] 属性或 Thread.SetApartmentState() 调用启用。STA 需要使用从根本上说是线程不安全的组件,例如窗口、剪贴板、拖放、Shell 对话框(例如 OpenFileDialog)和许多其他 COM coclass,其中一些您可能无法识别,因为它们是由 .NET 类包装的. STA 单元确保这些组件以线程安全的方式使用,从工作线程调用此类组件的方法会自动封送到创建它的线程。与 Control.Invoke() 完全等价。这种编组要求线程泵送一个消息循环以在正确的线程上分派调用。你的主线是在 Console.ReadLine() 调用中被阻止时不抽水。工作线程将在调用中停止,直到主线程再次开始抽水。死锁的可能性非常高,尽管您实际上并没有完全死锁,因为 ReadLine() 调用最终完成。值得注意的是,CLR 避免了这类死锁,当您使用lock关键字、在同步对象上调用 WaitOne() 或调用 Thread.Join()(.NET 编程中常见的阻塞调用)时,它会触发消息循环。但是,它不会为 Console.ReadLine() 执行此操作,至少在 Mark 所示的 .NET 4.0 解决方法之前是这样。

  • 一个高度投机的问题,由您观察到您通过等待创建表单来避免问题而触发。您可能在 64 位操作系统上遇到此问题,并且您的 EXE 项目的平台目标设置设置为“x86”而不是“AnyCPU”。在这种情况下,您的程序在 WOW64 仿真层中运行,该层允许 64 位操作系统执行 32 位程序。Windows 窗口管理器是一个 64 位子系统,调用它来向 32 位窗口发送通知会通过一个切换位的 thunk。窗口管理器传递 WM_SHOWWINDOW 消息时触发的 Form.Load 事件存在问题。它使仿真层处于困难状态,因为它多次穿过 64 位到 32 位边界。这个答案。此代码在 Load 事件触发时持有模拟器锁的可能性非常高。所以在 Load 事件中调用 Console.ReadLine() 可能会很麻烦,我希望任何 32 位工作线程在进行 api 调用时也会传递这个锁。高度推测,但如果您可以将平台目标更改为 AnyCPU,则易于测试。

鉴于解决方案非常简单,因此不确定是否值得追查此问题的原因。只是不要在主线程上调用 Console.ReadLine() 。而是在工作线程上调用它。当 COBOL 程序无响应时,这也会阻止您的 UI 冻结。但是请注意,如果您收到的任何内容也更新了您的 UI,您现在必须使用 Control.Begin/Invoke() 编组自己。

于 2012-11-22T15:49:49.643 回答
2

将 .NET 3.5/2.0 和 .NET 4.0 与 Reflector 进行比较:.NET 4.0 总是显式调用__ConsoleStream.WaitForAvailableConsoleInput(hFile)__ConsoleStream.ReadFileNative而我在 .NET 3.5/2.0 中找不到等效调用。

WaitForAvailableConsoleInput InternalCall检查管道,如果是这样,如果它有可用的数据或它已关闭,则避免等待它。


总结一下我所理解的是这个问题的当前状态:可以通过确保其他线程(AppDomains)在允许主线程继续ReadLine调用之前泵送消息来解决这个问题。

我认为这可能是一个(几乎)最终答案,因为这意味着在服务器版本期间有 Windows 消息正在进行,ReadLine这足以让我知道需要抽水。这归结为 Windows 文档需要提及需要/使用 Windows 消息传递的位置。

于 2012-11-21T02:19:47.987 回答
0

您是否尝试通过设置 Thread.IsBackground = true 将这些线程设置为后台线程

于 2012-11-21T10:34:02.070 回答