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

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),否则不会执行任何请求的线程。从那时起,所有请求的winforms将开始显示并按预期运行。
通过日志挖掘这种奇怪的行为,我发现线程正常执行,直到执行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线程将在尝试连接到数据库时在本机代码中冻结):

Threads vs StackTrace's


不是解决方案,而是一种变通方法:您的COBOL应用程序能否写入一个文本文件,然后您可以从.NET应用程序中读取该文件?这应该可以避免冻结,但可能比命令行解决方案慢。 - Christian Sauer
我不知道问题出在哪里,但你能否尝试切换到一个额外的线程来处理 While ... ReadLine 循环,并使用 IPC 来检索参数等?在这种情况下,看看是什么导致了程序挂起会很有趣。 - Mark Hurd
@MarkHurd:所有线程都会在Console.Readline上挂起,不管是哪个线程执行该命令(主线程或子线程)。我已经进行了这个测试。实际上,调用Console.ReadLine的线程在没有输入可用时挂起是可以的,但是所有其他线程都会在某些PInvoke调用上挂起-因为它们也在等待输入。 - J.Hudler
1
据我所了解,这并不是原始帖子的问题;事实上,我相信那部分工作正常。问题在于ReadLine在Win32API中阻塞,以一种使得所有托管线程都被卡住的方式,而不仅仅是应该被阻塞的线程。 - Mark Hurd
你能否使用.NET 3.5/2.0而不是.NET 4.0进行测试吗? - Mark Hurd
显示剩余6条评论
3个回答

4
首先,你正在做一件非常不寻常的事情,因此出现异常结果并不意外。其次,在Windows SDK文档中严格禁止你所做的事情。这意味着创建单线程公寓的线程永远不允许进行阻塞调用。
显然,你的工作线程在操作系统内部的某个锁上被阻塞,这个锁是由程序的主线程占用的。查看其中一个被阻塞的线程的调用堆栈可以帮助你确定可能存在的锁。这需要启用非托管代码调试,这样你就可以看到非托管堆栈帧,以及启用Microsoft符号服务器,这样你就可以获取Windows代码的调试符号,并从堆栈跟踪中理解所发生的事情。
没有可供查看的内容,我只能进行猜测:
  • Console.ReadLine()本身会获取一个内部锁,使控制台可以从多个线程安全地使用,串行化对Read()的调用。使用控制台方法的任何线程都可能遇到同样的锁。这不太可能是罪魁祸首,因为这些工作线程也不太可能使用控制台。

  • “严禁”角度与公寓线程相关,这是COM功能。STA由程序的Main()方法上的[STAThread]属性或Thread.SetApartmentState()调用启用,要使用基本上不安全的组件,如窗口、剪贴板、拖放、像OpenFileDialog等shell对话框和许多其他COM coclasses,其中一些您可能无法识别,因为它们被.NET类包装。STA公寓确保以线程安全的方式使用这些组件,从工作线程调用此类组件的方法会自动传送到创建它的线程。这相当于Control.Invoke()的确切等效项。这种封送需要线程通过消息循环来分派调用正确的线程。当主线程在Console.ReadLine()调用上被阻塞时,你的主线程是在循环消息的。工作线程将在调用上停顿,直到主线程重新开始循环消息。非常高的死锁几率,虽然你实际上不会完全死锁,因为ReadLine()调用最终会完成。值得注意的是,CLR避免这些类型的死锁,当你使用lock关键字、在同步对象上调用WaitOne()或调用Thread.Join()时,它会循环消息,这是.NET编程中常见的阻塞调用。然而,至少在.NET 4.0的解决方法(Mark所示)之前,它不会对Console.ReadLine()执行此操作。

  • 高度推测性的问题,由你观察到你可以通过等待窗体创建来避免问题引发的。如果你的EXE项目的平台目标设置为“x86”,而不是“AnyCPU”,则可能在64位操作系统上出现这个问题。在这种情况下,您的程序运行在WOW64仿真层中,该层允许64位操作系统执行32位程序。Windows窗口管理器是一个64位子系统,它向32位窗口发送通知的调用通过一个切换位数的thunk进行。Form.Load事件存在问题,当窗口管理器传递WM_SHOWWINDOW消息时触发。这使得仿真层处于困难状态,因为它穿越了64位到32位边界多次。这也是一个非常令人讨厌的异常吞噬问题的源头,其在此答案中有记录。很有可能这段代码在Load事件触发时持有一个仿真器锁。因此,在Load事件中调用Console.ReadLine()很可能会非常麻烦,我预期任何32位工作线程在进行api调用时也会通过该锁。高度推测性的问题,但如果您可以将平台目标更改为AnyCPU,则很容易测试。

考虑到解决方案非常简单,可能并不值得追究这个问题的原因。只需不要在主线程上调用Console.ReadLine(),而是在工作线程上调用它。这也可以防止当COBOL程序无响应时您的UI冻结。但请注意,如果您收到的内容还更新了UI,则现在必须使用Control.Begin/Invoke()进行调度。


非常感谢您提供如此详细和全面的答案。为了减少您的猜测,我会在我的问题中添加更多细节。我也会尽快尝试将本地堆栈跟踪放在这里。 - J.Hudler
我会选择第三个要点。使用 sgen.exe 预编译 XML 序列化程序。 - Hans Passant
将其设置为教育性解释的正确答案 - 这让我沉思我的代码设计,并澄清了问题所在。 - J.Hudler

2
使用Reflector比较.NET 3.5/2.0和.NET 4.0:.NET 4.0总是在__ConsoleStream.ReadFileNative中显式调用__ConsoleStream.WaitForAvailableConsoleInput(hFile),而我在.NET 3.5/2.0中找不到相应的调用。 WaitForAvailableConsoleInput InternalCall检查管道,如果有数据可用或已关闭,则避免等待管道。
简单概括一下我理解的当前问题状态:可以通过确保其他线程(AppDomains)在允许主线程继续到ReadLine调用之前泵送消息来解决它。
我认为这可以作为一个(几乎)最终答案,因为这意味着在服务器版本的ReadLine期间存在Windows消息,这对我来说足够了解到需要泵送。这归结于Windows文档需要提及何处需要/使用Windows消息。

刚试图将两个相关的库切换到3.5版本,但错误列表变得不那么小了。我还试图将这种情况移植到一个测试用例中并在此粘贴,但可能涉及一些“隐藏”的操作导致出现这种情况——简单的代码片段并不能使其发生。(无论如何,对我来说迁移到3.5版本不是一个选项)。 - J.Hudler
FYI,当我想允许来自连接到普通控制台窗口(在WinXP和Win2k3中)的DotLisp REPL的剪贴板交互时,我看到了同样的问题。我有一个后台线程,不断调用(System.Threading.Thread:CurrentThread.Join 0) (System.Windows.Forms.Application:DoEvents)(System.GC:WaitForPendingFinalizers) (System.Threading.Thread:Sleep 55) - Mark Hurd
现在我明白了故事的寓意,因此我将授予你从一开始就提供的帮助的赏金。 - J.Hudler

0

你尝试过将这些线程设置为后台线程吗?可以通过设置 Thread.IsBackground = true 来实现。


1
刚试了一下你的方法,但没有成功。另外,我不能将子线程作为后台,因为我希望打开的窗体即使在菜单(主线程)关闭时仍然可以运行。 - J.Hudler
据我理解,您的应用程序是一个控制台应用程序并运行表单,如果我没错的话,您可以考虑将这些表单作为独立的进程运行,而不是线程。为此,您可以将表单开发为自己的应用程序,并找到一种方法将参数传递给您的表单,例如管道或套接字等。我知道这会稍微改变您的设计,但我认为这可能是 .Net 是 COM 对象的问题,而不仅仅是我认为 .Net 线程不像 C++ CreateThread。我不确定这一点,但您可以深入挖掘一下。 - Ibrahim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接