Console.Write() будет висеть в WPF, но работает в приложении консоли

Пожалуйста, прочитайте ответ Скоттом Чемберленом, чтобы узнать, почему это связано с WINAPI.

Создайте новое приложение WPF в Visual Studio и измените код в 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);
            }
        }
    }
}

Вопросы:

  • Почему второй вызов Console.Write() зависает в приложении WPF?
  • Почему поведение в консольном приложении отличается?
  • Почему это происходит, только если строка является строкой \0? (Он отлично работает, если вы используете 1024 пробела.)

Ответ 1

Базовый Объяснение: Он зависает, потому что буфер Console.Write записывает перед тем, как текст отображается, заполняется и не сливается для приложений WPF при передаче нулевых символов (\0) для по причинам, не зависящим от меня.


Подробное объяснение:. Когда вы вызываете 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 его читает визуальная студия.

Сам зависание исходит из того факта, что в буфере имеется ограниченное пространство, и только так много текста может быть помещено в очередь, прежде чем дескриптор должен заблокировать новые входящие запросы, чтобы существующая информация могла вытечь. Похоже, что дескриптор консольного приложения обрабатывает большое количество символов \0, но дескриптор, который генерирует WPF, не может. Если это отличие от него - это другой тип дескриптора или от процессора на другой стороне ручка, читающего данные по-разному, я не знаю.

Надеюсь, у кого-то, у кого больше опыта, чем у меня с вызовом Windows API WriteFile, который может объяснить различия между двумя типами дескрипторов и даст нам лучшую идею, если это из-за типа дескриптора или из-за получающей программы.

Ответ 2

Это работает на моей машине без проблем (Winodws 8.1 VS2013). Проблема не имеет ничего общего с консольным приложением. Как объяснил Скотт, происходит некоторая блокировка. Он работает, когда не запускается под отладчиком

  • Как приложение WPF
  • Как консольное приложение

Он будет зависать, когда вы пытаетесь отладить приложение. Более глубокая причина заключается в том, что вы отправляете \0 по трубе. У трубы есть буфер отправки (около 4 КБ), прежде чем он блокирует дальнейшую запись на него. Это то, что вы видите как висячий вызов WriteFile в файле kernel32.dll. Чтобы иметь возможность блокировать, должен быть кто-то, кто хочет читать из вашей трубы. В этом случае это VS, пытающийся вывести ваш stdout в окно вывода отладчика. Когда никто не слушает трубку, действует как нулевое устройство, которое никогда не будет блокироваться.

Теперь вернемся к вопросу, почему он работает со всеми строками, кроме \0? Это должно сделать, как прекратить чтение из трубы. Приемник может прекратить чтение из канала, когда он получает \0 в качестве единственного сообщения. Это сигнал, что процесс вышел, и никакие дополнительные данные не будут записаны в него. С вашими сообщениями \0 вы нарушаете этот неявный контракт и отправляете дополнительные данные по каналу, но ваш клиент (VS) перестает вас слушать. На самом деле это не API, но, похоже, это общее соглашение. В .NET вы получите для чтения async-каналов, например. null как последнее сообщение задний. Другие приложения (например, VS), похоже, обрабатывают сообщения \0 аналогичным образом и предполагают, что автор вышел.

Законно просто закрыть дескриптор трубы в этом случае ReadFile возвращает false. Наше вы также можете написать сообщение длиной 0 байтов в трубе. Или вы можете написать нулевой массив 1024 КБ в трубе. Читатель ваших сообщений должен решить, является ли это сигналом для прекращения чтения из вашей трубы.

Обновление 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

Буфер содержит данные 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

В этом вся магия. Читатель просто перестает читать, и ваше приложение блокируется при заполнении буфера отправителя. Никакой магии не было. Все зависит от поведения читателя.

Ответ 3

Так как WPF не имеет без знака дескриптора окна консоли. Я не могу увидеть реализацию метода Write, но вы можете видеть, что большинство публичных свойств статического класса Console возвращает ошибку ввода-вывода с "Message =". Недопустимый дескриптор. \R\n ".

Если вы хотите отобразить окно консоли, в приложении WPF вам необходимо выполнить код в неуправляемой библиотеке kernel32.dll.

См. Нет вывода на консоль из приложения WPF?