【问题标题】:Console.In.ReadLine (redirected StdIn) freezes all other threadsConsole.In.ReadLine(重定向的 StdIn)冻结所有其他线程
【发布时间】:2012-11-18 03:23:49
【问题描述】:

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

<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)上运行。
  • 更新了上述 sn-p 中的 Sub Main() 属性。
  • 试图将Console.ReadLine 放入工作线程。没有解决(见下图)。
  • COBOL 应用程序不会冻结,因为它是在单独的 OS 进程中执行的。管道恰好是我的 IPC 方法。本例中的 COBOL 应用程序只写入参数,不等待响应。
  • 堆栈跟踪如下图所示(线程 PRX001235 在连接到数据库之前和有效加载表单之前反序列化 xml 配置文件 - 在这种情况下,似乎仍在托管代码中 - 有时 @ 987654335@线程在尝试连接数据库时会冻结在本机代码中):

【问题讨论】:

  • 不是一种解决方案,而是一种解决方法:您的 COBOL 应用程序可以写入一个 txt 文件,您可以从您的 .NET 应用程序中读取该文件吗?这应该可以避免冻结,但可能比命令行解决方案慢。
  • 嗨@ChristianSauer。我真的在考虑你建议的这个解决方案。我坚持使用管道的主要原因是,我可以在流关闭时终止从属应用程序 (.NET)(即 COBOL 应用程序也终止)。对于文本文件,我失去了这种“跟踪”优势。
  • 我不知道问题出在哪里,但是您可以尝试更改为仅处理While ... ReadLine 循环的额外线程,并使用IPC 来检索参数等吗?看看这种情况下发生了什么会很有趣。
  • 同样在您的' Deal with parameters here' Start new thread. Generally showing a Winforms form. 中,我认为您有变量被“提升”到Sub() 的闭包中。您是否有机会在为避免它们而添加的锁中出现竞争条件或死锁?
  • @ChristianSauer:据我了解,这不是 OP 的问题;事实上,我相信这部分工作正常。问题是 ReadLine 在 Win32API 中阻塞,导致所有托管线程都被阻塞,而不仅仅是应该阻塞的线程。

标签: .net vb.net multithreading pipe


【解决方案1】:

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

很明显,您的工作线程被操作系统内部某处的锁阻塞,该锁由您的程序的主线程占用。查看其中一个被阻塞线程的调用堆栈以识别可能的锁会很有帮助。这需要启用非托管代码调试,以便您可以查看非托管堆栈帧并启用 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 所示的 .NET 4.0 解决方法之前。

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

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

【讨论】:

  • 感谢您提供如此详细而彻底的回答。为了减少您的投机假设,我在我的问题中添加了更多细节。我也打算尽快把原生堆栈跟踪放在这里。
  • 我会坚持使用第三个子弹。使用 sgen.exe 预编译 XML 序列化程序。
  • 设置为教育解释的正确答案 - 这让我思考我的代码设计并澄清问题所在。
【解决方案2】:

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

WaitForAvailableConsoleInputInternalCall 检查管道,如果有,则避免等待管道是否有可用数据或已关闭。


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

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

【讨论】:

  • 刚刚尝试将两个涉及的库切换到3.5,但是错误列表变得不那么小。我也在尝试将这种情况移植到测试用例并粘贴到此处,但可能涉及一些“隐藏”操作导致这种情况发生 - 简单的代码,因为 sn-p 无法实现。 (无论如何,迁移到 3.5 对我来说不是一个选择)。
  • 仅供参考,当我希望允许从 DotLisp REPL 连接到普通控制台窗口(在 WinXP 和 Win2k3 中)的剪贴板交互时,我看到了同样的事情。我有一个后台线程不断调用(System.Threading.Thread:CurrentThread.Join 0) (System.Windows.Forms.Application:DoEvents) (System.GC:WaitForPendingFinalizers) (System.Threading.Thread:Sleep 55)
  • 现在我明白了这个故事的寓意,我从一开始就奖励你的帮助。
【解决方案3】:

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

【讨论】:

  • 刚刚尝试了您的方法,但没有成功。此外,a 不能将子线程作为背景,因为即使菜单(主线程)关闭,我也希望打开的表单继续运行。
  • 据我了解,您的应用程序是控制台应用程序并运行表单,如果我是正确的,您可以考虑在进程外运行这些表单......而不是线程......为此,您将表单开发为它自己的应用程序,并找到一种方法将参数传递给您的表单、管道或套接字……等等。我知道这会稍微改变你的设计……但我认为 .Net 作为 COM 对象是一个问题,而不是只有我相信 .Net 线程不像 c++ CreateThread .. 我不确定,但你可能会挖掘它
猜你喜欢
  • 2016-04-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-06
  • 2011-01-13
相关资源
最近更新 更多