Console.In.ReadLine(重定向StdIn)冻结所有其他线程

时间:2012-11-18 03:23:50

标签: .net vb.net multithreading pipe

我正在使用管道将遗留应用程序(用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正在被执行被迫关闭)。从那时起,所有请求的winforms将开始显示并按预期运行。

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

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

我该怎么办?有什么建议吗?

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

更新消息

我可以通过等待子线程来完成加载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线程将冻结本机代码:

Threads vs StackTrace's

3 个答案:

答案 0 :(得分:4)

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

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

没有人看,我将不得不推测:

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

  • 严格的 verboten 角度与公寓线程(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 4.0所示的.NET 4.0解决方法之前。

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

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

答案 1 :(得分: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调用之前抽取消息来解决。

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

答案 2 :(得分:0)

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