在WPF中,Console.Write()会挂起,但在控制台应用程序中可以正常工作。

11
请阅读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. 为什么在WPF应用程序中第二个Console.Write()调用会挂起?
  2. 为什么控制台应用程序的行为不同?
  3. 为什么它只会发生在字符串是\0的情况下?(如果您输入1024个空格,则正常运行。)

我做过类似的事情,但它不是阻塞调用。调试你的代码。也许错误是其他什么东西。 - Muhammad Umar
@MuhammadUmar 这是我唯一拥有的代码。我再次检查过,它会阻止程序运行。你测试的是哪个版本的 .NET? - Roman Byshko
1
我的错误。我也重新创建了这个问题。 :) - Muhammad Umar
2
我找到了它挂在哪里(它在对WriteFile的本地调用上被阻塞),但我还没有弄清楚为什么它会挂起(因为缓冲区已满,但我不知道是什么导致它发生)。还在寻找中。 - Scott Chamberlain
1
它也会在WinForms应用程序中挂起,但在停止之前可以通过4个循环。 - Walt Ritscher
显示剩余4条评论
3个回答

6

基本解释:它会卡住,因为在WPF应用程序中,当传递空字符(\0)时,缓冲区Console.Write在文本显示之前写入的内容过多而未被清空。


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

首先,如果使用控制台应用程序检查句柄类型,你将得到类型为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 生成的句柄不能。如果这种差异是由于它是一种不同类型的句柄还是由于句柄另一端的处理器以不同的方式读取数据,我不知道。
希望有更多经验的人能够解释两种句柄类型之间的差异,并让我们更好地了解这是由于句柄类型还是由于接收程序的原因。

2
这在我的机器上没有问题(Winodws 8.1 VS2013)。这个问题与控制台应用程序无关。正如Scott所解释的那样,有一些阻塞发生了。 当不在调试器下运行时可以工作。
  • 作为WPF应用程序
  • 作为控制台应用程序

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

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

在这种情况下,直接关闭管道句柄是合法的ReadFile返回false。或者你也可以向管道写入一个长度为0字节的消息。或者您可以向管道写入1024 KB空数组。由您的消息的阅读器决定是否这是停止从您的管道中读取的信号。

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

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

缓冲区包含数据 a 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

这就是整个过程的奥妙所在。读者只需停止阅读,当发送缓冲区已满时,你的应用程序将会被阻塞。没有任何魔法。一切都取决于读者的行为。


你的答案的第一部分可以在注释中找到,已经存在了。而且:如果你只发送“\0”,它不会阻塞...所以请不要猜测。 - Roman Byshko
没有猜测。我已经更新了答案,以显示它完全取决于读者的行为。 - Alois Kraus
在我的WinForms应用程序中,添加Console.WriteLine("\0");这一行将可靠地导致后续对Console.Writeline的调用不起作用。听起来像是一个bug。 - Tom Bushell

0

因为 WPF 没有指向控制台窗口的非托管句柄,我无法看到 Write 方法的实现,但是你可以看到静态 Console 类的大多数公共属性返回带有 "Message =“The handle is invalid.\r\n" 的 I/O 错误。

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

请参见 WPF 应用程序没有控制台输出?


它确实具有未托管的句柄,将1024个空值替换为单词“Test”,然后查看Visual Studio中的“Output”窗口,您将在那里看到打印出来的“Test”。 - Scott Chamberlain
@ScottChamberlain 我仍然会遇到大量的System.IO.IIOException,对于控制台类的大多数公共属性,在任何Console.Write发生之前或之后都会出现No Handle Message。但是如果我运行ConsoleManager.Show(),控制台就会获得句柄,并且属性将填充数据。然后可以正常显示1024个null。我猜测Console.Write试图将代码马歇尔到未管理的线程中,该线程尝试访问控制台窗口,但由于它不存在,因此挂起。没有看到Console.Write实现很难说为什么。 - Michal Ciechan
你是在看 Console.Out,它是一个 TextWriter 还是在看 Console.OpenStandardOutput(),它是 Console.Out 使用的基础流。你还可以通过使用 Microsoft Reference Source 查看 Console.Write 的实现,甚至在调试时 stepping into .NET 源代码,如果你 正确设置了 Visual Studio(这就是我发布答案的方式)。 - Scott Chamberlain

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