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?