Process.Start() зависает при работе в фоновом потоке

Я весь день искал все проблемы. После выполнения исследований и большого количества проб и ошибок, похоже, я смог сузить проблему до того, что мой призыв к process.Start() не работает в потоке таймера. Приведенный ниже код работает при работе в основном потоке. Поместите этот точный код в обратный вызов таймера, и он зависает. Зачем? Как заставить его работать с таймером?

private static void RunProcess()
{
    var process = new Process();

    process.StartInfo.FileName = "cmd";
    process.StartInfo.Arguments = "/c exit";
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardError = true;
    process.StartInfo.RedirectStandardInput = true;
    process.StartInfo.RedirectStandardOutput = true;

    process.Start();  // code hangs here, when running on background thread

    process.StandardOutput.ReadToEnd();

    process.WaitForExit();
}

ИЗМЕНИТЬ

В качестве теста я использовал этот точный код на другом ноутбуке, и у меня возникла та же проблема. Это полный код, который можно вставить в консольное приложение. process.Start() зависает, но как только я удаляю любую клавишу до конца, process.Start() завершается до завершения программы.

private static System.Timers.Timer _timer;
private static readonly object _locker = new object();

static void Main(string[] args)
{
    ProcessTest();

    Console.WriteLine("Press any key to end.");
    Console.ReadKey();
}
private static void ProcessTest()
{
    Initialize();
}
private static void Initialize()
{
    int timerInterval = 2000;
    _timer = new System.Timers.Timer(timerInterval);
    _timer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);
    _timer.Start();
}
private static void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
    if (!Monitor.TryEnter(_locker)) { return; }  // Don't let  multiple threads in here at the same time.
    try
    {
        RunProcess();
    }
    finally
    {
        Monitor.Exit(_locker);
    }
}
private static void RunProcess()
{
    var process = new Process();
    process.StartInfo.FileName = "cmd";
    process.StartInfo.Arguments = "/c exit";
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardError = true;
    process.StartInfo.RedirectStandardInput = true;
    process.StartInfo.RedirectStandardOutput = true;
    process.Start();  // ** HANGS HERE **
    process.StandardOutput.ReadToEnd();
    process.WaitForExit();
}

Ответ 1

Есть много дублирующих вопросов по этой проблеме, которые точно не соответствуют вашему делу. Вы можете увидеть эту проблему, используя окно отладки Debug + Windows + Threads. Найдите поток таймера и дважды щелкните его. Посмотрите на окно Call Stack, чтобы увидеть:

mscorlib.dll!System.Console.InputEncoding.get() + 0x66 bytes    
System.dll!System.Diagnostics.Process.StartWithCreateProcess(System.Diagnostics.ProcessStartInfo startInfo) + 0x7f5 bytes   
System.dll!System.Diagnostics.Process.Start() + 0x88 bytes  
ConsoleApplication70.exe!Program.RunProcess() Line 43 + 0xa bytes   C#
ConsoleApplication70.exe!Program.OnTimerElapsed(object sender, System.Timers.ElapsedEventArgs e) Line 28 + 0x5 bytes    C#
    // etc...

Нить зашла в тупик на получателе свойств Console.InputEncoding. Используется классом Process для определения того, какая кодировка должна использоваться для перевода перенаправленного вывода процесса в строки.

Это специфично для .NET 4.5, оно также будет влиять на приложения, которые нацелены на 4.0 на машине с установленным 4.5, поскольку она не является бок о бок версией .NET. Тупик вызван вызовом метода Console.ReadKey() в вашем основном потоке. Теперь он получает блокировку, предотвращающую взаимодействие других потоков с консолью. Это было довольно глобальное изменение в программном обеспечении Microsoft, CRT, который используется в приложениях C/С++, созданных VS2012, также добавил эту блокировку. Точная причина для меня не так понятна, но, безусловно, нужно что-то делать, когда консольный вывод не смешивается с консольным вводом, когда ваша программа запрашивает ввод. Именно поэтому свойство InputEncoding необходимо также блокировать, но это немного сложно объяснить, но он соответствует шаблону сериализации доступа к вводу в консоль. Это, конечно, является большим сюрпризом для многих программистов, особенно тех, которые пишут небольшие тестовые приложения, которые проверяют многопоточный код, как и вы. Бит неудачи для TDD.

Обходной путь немного неприятный, TDD мудрый, вам нужно прекратить использование Console.ReadKey(), чтобы избежать тупика. Реальные программы будут использовать метод WaitOne() для AutoResetEvent, чтобы знать, что рабочий поток завершил выполнение. Или CountDownEvent.Wait(), больше в порядке с проверкой кода пару раз. Etcetera.


UPDATE: этот сценарий блокировки был разрешен в обновлении службы для .NET 4.5. Включите Windows Update на своем компьютере, чтобы получить его.