【问题标题】:Console.Write() will hang in WPF, but works in Console applicationConsole.Write() 将在 WPF 中挂起,但在控制台应用程序中有效
【发布时间】:2014-05-05 21:34:41
【问题描述】:

请阅读 Scott Chamberlain 的回答,了解为什么它与 WINAPI 相关。

在 Visual Studio 中创建一个新的 WPF 应用程序并更改 MainWindow.xaml.cs 中的代码,如下所示。运行应用程序。该代码将在第二次调用Console.Write() 时挂起。

MainWindow.xaml.cs

using System;
using System.Text;
using System.Windows;

namespace TestWpf
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            byte[] msg = new byte[1024];

            string msgStr = Encoding.Default.GetString(msg);

            for (int i = 0; i < 10; i++)
            {
                Console.Write(msgStr);
            }
        }
    }
}

现在在 Visual Studio 中创建一个新的控制台应用程序并更改 Program.cs 中的代码,如下所示。运行应用程序。它将成功运行,即不会挂起。

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            byte[] msg = new byte[1024];

            string msgStr = Encoding.Default.GetString(msg);

            for (int i = 0; i < 100; i++)
            {
                Console.Write(msgStr);
            }
        }
    }
}

问题:

  1. 为什么对 Console.Write() 的第二次调用在 WPF 应用程序中挂起?
  2. 为什么控制台应用程序的行为不同?
  3. 为什么只有当字符串是\0 时才会发生这种情况? (如果你做 1024 个空格就可以了。)

【问题讨论】:

  • 我做过类似的事情,但它没有阻塞呼叫。调试你的代码。可能是错误是别的。
  • @MuhammadUmar 这是我唯一的代码。刚刚再次检查,它阻塞。您正在测试哪个版本的 .NET?
  • 我的错误。我也重新创建了这个问题。 :)
  • 我发现它挂起的是什么(它阻塞了对 WriteFile 的本机调用),但我还没有弄清楚它挂起的为什么(这是因为缓冲区是已满,但我不知道是什么导致它发生)。还在寻找。
  • 它也挂在一个 WinForms 应用程序中,但在停止之前它会经过 4 个循环。

标签: c# wpf winapi console-application


【解决方案1】:

基本解释:它挂起是因为在显示文本之前写入的缓冲区 Console.Write 已满,并且在为 WPF 应用程序传入空字符 (\0) 时没有耗尽我不知道的原因。


详细说明:当您调用Console.Write 时,它会创建一个Handle 以将其数据输出到并最终在该句柄上调用WriteFile。句柄的另一端需要处理写入它的数据,然后将控制权返回给调用者。我发现 WPF 和控制台应用程序之间有两个主要区别:

首先,如果您使用控制台应用程序inspect the handle type,您将获得 FILE_TYPE_CHAR 类型的句柄,从 WPF 获得 FILE_TYPE_PIPE

Console.Write(msgStr);

var cOut = Console.OpenStandardOutput();
var handle = cOut.GetType().GetField("_handle", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(cOut);
var method = Type.GetType("Microsoft.Win32.Win32Native").GetMethod("GetFileType", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
var type = method.Invoke(null, new object[] { handle });
Debugger.Break();

第二,接收端处理句柄的方式不同。在控制台应用程序中,句柄由 conhost.exe 读取,在 WPF 中,它由 Visual Studio 读取。

挂起本身的原因在于缓冲区中的空间有限,并且在句柄必须阻止新传入请求之前只能将这么多的文本排队,以便现有信息可以耗尽。控制台应用程序的句柄似乎可以处理大量 \0 字符,但 WPF 生成的句柄不能。如果这种差异是由于它是一种不同类型的句柄,还是来自句柄另一侧的处理器以不同方式读取数据,我不知道。

希望在 Windows API 调用 WriteFile 方面比我有更多经验的人可以解释两种句柄类型之间的差异,并且如果这是因为句柄类型还是因为接收程序,会给我们一个更好的想法.

【讨论】:

    【解决方案2】:

    这在我的机器上运行没有问题(Winodws 8.1 VS2013)。该问题与控制台应用程序无关。正如斯科特解释的那样,有一些障碍正在发生。 它在不在调试器下运行时有效

    • 作为 WPF 应用程序
    • 作为控制台应用程序

    当您尝试调试应用程序时它会挂起。更深层次的原因是您通过管道发送 \0 。管道在阻止进一步写入之前有一个发送缓冲区(大约 4 KB)。这就是您在 kernel32.dll 中看到的挂起的 WriteFile 调用。为了能够阻止必须有人想从您的管道中读取。在那种情况下,它是 VS 试图让你的标准输出到调试器输出窗口。当没有人在听时,管道充当永远不会阻塞的空设备。

    现在回到这个问题,为什么它适用于除 \0 之外的所有字符串?这与如何停止从管道读取有关。当接收器接收到 \0 作为唯一消息时,接收器可以停止从管道读取。这是进程已退出的信号,不会向其写入更多数据。使用您的 \0 消息,您违反了该隐式合同并通过管道发送更多数据,但您的客户端(VS)已停止收听您。 这实际上不是一个 API,但似乎是一个共同的协议。在 .NET 中,您可以获得异步管道读取,例如null 作为最后一条消息。其他应用程序(例如 VS)似乎以类似的方式处理 \0 消息并假设编写器已退出。

    ReadFile 返回 false 的情况下,简单地关闭管道句柄是合法的。我们还可以将 0 字节长度的消息写入管道。或者您可以将一个 1024 KB 的空数组写入管道。消息的读者可以决定这是否是停止从管道读取的信号。

    更新 1 由于至少有一位评论者认为这里的逻辑不够,这是调试 VS 的结果。 VS 确实通过 ReadFile 从管道中读取数据

    vsdebug!CReader::ReadPipe
    

    检查第一个字节是否为 0,然后导致线程终止。如果第一个字节不是 0,则将其视为 unicode 字符串并复制到调试器输出窗口中显示的字符串缓冲区。您可以通过发送来轻松验证这一点,例如1000 个 c 字符将显示在缓冲区中。然后,您可以遵循与 1000 0 字节不同的控制流。 原来相关的部分是:

    0:048> db ebp-420
    1973f794  00 00 00 00 38 63 71 10-fe 03 00 00 92 82 b5 45  ....8cq........E
    1973f7a4  63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63  cccccccccccccccc
    1973f7b4  63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63  cccccccccccccccc
    1973f7c4  63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63  cccccccccccccccc
    1973f7d4  63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63  cccccccccccccccc
    1973f7e4  63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63  cccccccccccccccc
    1973f7f4  63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63  cccccccccccccccc
    1973f804  63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63  cccccccccccccccc
    

    缓冲区包含 ebp-410 的数据,其中是我们的 c 字符。在此之前,存储了缓冲区大小和文件句柄。

        0:048> u 5667dd48  L50
        vsdebug!ReaderThreadStart+0x72:
        **5667dd48 80bdf0fbffff00  cmp     byte ptr [ebp-410h],0**  check if first byte is 0 
        5667dd4f 7446            je      vsdebug!ReaderThreadStart+0xc1 (5667dd97)
        5667dd51 899decfbffff    mov     dword ptr [ebp-414h],ebx
        5667dd57 897dfc          mov     dword ptr [ebp-4],edi
        5667dd5a 8d85f0fbffff    lea     eax,[ebp-410h]
        5667dd60 53              push    ebx
        5667dd61 53              push    ebx
        5667dd62 50              push    eax
        5667dd63 8d8decfbffff    lea     ecx,[ebp-414h]
        5667dd69 e8f5960200      call    vsdebug!CVSUnicodeString::CopyString (566a7463)
        5667dd6e c745fc02000000  mov     dword ptr [ebp-4],2
        5667dd75 8b4e30          mov     ecx,dword ptr [esi+30h]
        5667dd78 6aff            push    0FFFFFFFFh
        5667dd7a ffb5ecfbffff    push    dword ptr [ebp-414h]
        5667dd80 e84453f6ff      call    vsdebug!CMinimalStreamEx::AddStringW (565e30c9)
        5667dd85 834dfcff        or      dword ptr [ebp-4],0FFFFFFFFh
        5667dd89 8d8decfbffff    lea     ecx,[ebp-414h]
        5667dd8f 53              push    ebx
        5667dd90 e86339f5ff      call    vsdebug!CVSVoidPointer::Assign (565d16f8)
        5667dd95 eb03            jmp     vsdebug!ReaderThreadStart+0xc4 (5667dd9a)
       ** 5667dd97 897e28          mov     dword ptr [esi+28h],edi ** When 0 go here and sleep 200ms
        5667dd9a 68c8000000      push    0C8h
        5667dd9f ff151c228056    call    dword ptr [vsdebug!_imp__Sleep (5680221c)]
        5667dda5 e978caf9ff      jmp     vsdebug!ReaderThreadStart+0xd4 (5661a822)
        5667ddaa e87ffc0d00      call    vsdebug!__report_rangecheckfailure (5675da2e)
        5667ddaf cc              int     3
        5667ddb0 b9fe030000      mov     ecx,3FEh
        5667ddb5 899de4fbffff    mov     dword ptr [ebp-41Ch],ebx
        5667ddbb 3bc1            cmp     eax,ecx
        5667ddbd 7702            ja      vsdebug!CReader::Stop+0x84 (5667ddc1)
        5667ddbf 8bc8            mov     ecx,eax
        5667ddc1 53              push    ebx
        5667ddc2 8d85e4fbffff    lea     eax,[ebp-41Ch]
        5667ddc8 50              push    eax
        5667ddc9 51              push    ecx
        5667ddca 8d85f0fbffff    lea     eax,[ebp-410h]
        5667ddd0 50              push    eax
        5667ddd1 ff762c          push    dword ptr [esi+2Ch]
        5667ddd4 ff1590218056    call    dword ptr [vsdebug!_imp__ReadFile (56802190)]
        5667ddda 85c0            test    eax,eax
        5667dddc 0f845a80f9ff    je      vsdebug!CReader::Stop+0x117 (56615e3c)
        5667dde2 8b85e4fbffff    mov     eax,dword ptr [ebp-41Ch]
        5667dde8 85c0            test    eax,eax
        5667ddea 0f844c80f9ff    je      vsdebug!CReader::Stop+0x117 (56615e3c)
        5667ddf0 b900040000      mov     ecx,400h
        5667ddf5 3bc1            cmp     eax,ecx
        5667ddf7 736c            jae     vsdebug!CReader::Stop+0x125 (5667de65)
        5667ddf9 889c05f0fbffff  mov     byte ptr [ebp+eax-410h],bl
        5667de00 40              inc     eax
        5667de01 3bc1            cmp     eax,ecx
        5667de03 7360            jae     vsdebug!CReader::Stop+0x125 (5667de65)
        5667de05 889c05f0fbffff  mov     byte ptr [ebp+eax-410h],bl
        5667de0c 389df0fbffff    cmp     byte ptr [ebp-410h],bl
        5667de12 0f842480f9ff    je      vsdebug!CReader::Stop+0x117 (56615e3c)
        5667de18 899decfbffff    mov     dword ptr [ebp-414h],ebx
        5667de1e c745fc01000000  mov     dword ptr [ebp-4],1
        5667de25 8d85f0fbffff    lea     eax,[ebp-410h]
        5667de2b 53              push    ebx
        5667de2c 53              push    ebx
        5667de2d 50              push    eax
        5667de2e 8d8decfbffff    lea     ecx,[ebp-414h]
        5667de34 e82a960200      call    vsdebug!CVSUnicodeString::CopyString (566a7463)
        5667de39 c745fc02000000  mov     dword ptr [ebp-4],2
        5667de40 8b4e30          mov     ecx,dword ptr [esi+30h]
        5667de43 6aff            push    0FFFFFFFFh
        5667de45 ffb5ecfbffff    push    dword ptr [ebp-414h]
        5667de4b e87952f6ff      call    vsdebug!CMinimalStreamEx::AddStringW (565e30c9)
        5667de50 834dfcff        or      dword ptr [ebp-4],0FFFFFFFFh
        5667de54 8d8decfbffff    lea     ecx,[ebp-414h]
        5667de5a 53              push    ebx
        5667de5b e89838f5ff      call    vsdebug!CVSVoidPointer::Assign (565d16f8)
        5667de60 e9d77ff9ff      jmp     vsdebug!CReader::Stop+0x117 (56615e3c)
        5667de65 e8c4fb0d00      call    vsdebug!__report_rangecheckfailure (5675da2e)
        5667de6a cc              int     3
        5667de6b b81a7e5d56      mov     eax,offset vsdebug!ATL::CAtlMap<unsigned long,CScriptNode *,ATL::CElementTraits<unsigned long>,ATL::CElementTraits<CScriptNode *> >::~CAtlMap<unsigned long,CScriptNode *,ATL::CElementTraits<unsigned long>,ATL::CElementTraits<CScriptNode *> >+0x15 (565d7e1a)
        5667de70 c3              ret
        5667de71 b84c8e5d56      mov     eax,offset vsdebug!ATL::CAtlMap<unsigned long,ATL::CComPtr<IVsHierarchyEvents>,ATL::CElementTraits<unsigned long>,ATL::CElementTraits<ATL::CComPtr<IVsHierarchyEvents> > >::~CAtlMap<unsigned long,ATL::CComPtr<IVsHierarchyEvents>,ATL::CElementTraits<unsigned long>,ATL::CElementTraits<ATL::CComPtr<IVsHierarchyEvents> > >+0x15 (565d8e4c)
        5667de76 c3              ret
        5667de77 6857000780      push    80070057h
        5667de7c e8df0af6ff      call    vsdebug!treegrid::IGridView::CleanupItems (565de960)
    **    0:048> u 5661a822  ** Jump here
        vsdebug!ReaderThreadStart+0xd4:
        5661a822 395e28          cmp     dword ptr [esi+28h],ebx
        5661a825 74d0            je      vsdebug!ReaderThreadStart+0x26 (5661a7f7)
        5661a827 ff7624          push    dword ptr [esi+24h]
        5661a82a ff157c228056    call    dword ptr [vsdebug!_imp__SetEvent (5680227c)]
        5661a830 53              push    ebx
    **    5661a831 ff1508218056    call    dword ptr [vsdebug!_imp__ExitThread (56802108)]  ** Stop reading
    

    这就是围绕它的全部魔力。阅读器只是停止阅读,当发送者缓冲区已满时,您的应用程序将阻塞。不涉及魔法。这完全取决于读者的行为。

    【讨论】:

    • 您的答案的第一部分可以在 cmets 中找到并且已经存在。并且:如果你只是发送“\0”它不会阻塞......所以请不要猜测。
    • 没有猜测。我已经更新了答案,表明它完全取决于读者的行为。
    • 在我的 WinForms 应用程序中,添加行 Console.WriteLine("\0");将可靠地导致对 Console.Writeline 的后续调用什么都不做。对我来说听起来像是一个错误。
    【解决方案3】:

    因为 WPF 没有控制台窗口的未管理句柄。我无法看到 Write 方法的实现,但您可以看到静态控制台类的大多数公共属性返回一个 IO 错误,并显示“Message = “句柄无效。\r\n”。

    如果要显示控制台窗口,在 WPF 应用程序中需要执行 kernel32.dll 非托管库中的代码。

    No output to console from a WPF application?

    【讨论】:

    • 它确实有一个未管理的句柄,将 1024 空值替换为单词“Test”,然后查看 Visual Studio 中的Output 窗口,您会看到那里打印了“Test”。
    • @ScottChamberlain 对于控制台类的大多数公共属性,在任何 Console.Write 发生之前或之后,我仍然会收到大量带有无句柄消息的 System.IO.IIOException。但是如果我运行 ConsoleManager.Show(),Console 会获取句柄并且属性会填充数据。然后可以正常显示 1024 个空值。我猜 Console.Write 试图将代码编组到试图访问控制台窗口的非托管线程,但由于它不存在,它挂起。很难说为什么没有看到 Console.Write 的实现
    • 您查看Console.Out 的位置是TextWriter 或查看Console.OpenStandardOutput(); 的位置,这是Console.Out 使用的基础流。您还可以通过使用Microsoft Reference Source 来查看Console.Write 的实现,如果您set up visual studio correctly(这就是我发布答案的方式),甚至可以在调试时进入.NET 源代码
    猜你喜欢
    • 2015-10-13
    • 1970-01-01
    • 2015-06-21
    • 1970-01-01
    • 2013-06-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多