Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Process.Start() hangs when running on a background thread

I've been troubleshooting all day. After doing some research and a lot of trial and error, it seems I've been able to narrow down the issue to the fact that my call to process.Start() doesn't work on a timer thread. The code below works when running on the main thread. Put that exact same code in a timer callback, and it hangs. Why? How do I get it to work with a timer?

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();
}

EDIT

As a test, I used this exact same code on another laptop, and I experienced the same problem. This is complete code that can be pasted into a console app. process.Start() hangs, but as soon as I hit any key to end, process.Start() completes before the program ends.

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();
}
like image 355
Bob Horn Avatar asked Apr 24 '13 21:04

Bob Horn


1 Answers

There are a lot of duplicate questions about this problem, none that exactly fits your case. You can see the problem by using the debugger's Debug + Windows + Threads window. Locate the timer thread and double-click it. Look at the Call Stack window to see:

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...

The thread is deadlocked on the Console.InputEncoding property getter. Which is used by the Process class to figure out what encoding needs to be used to translate the redirected output of the process into strings.

This is specific to .NET 4.5, it will also affect apps that target 4.0 on a machine that has 4.5 installed since it is not a side-by-side version of .NET. The deadlock is caused by the Console.ReadKey() method call in your main thread. Which now acquires a lock that prevents other threads from messing with the console. This has been a fairly global change across Microsoft software, the CRT that is used in C/C++ apps created by VS2012 also added this lock. The exact reason isn't that clear to me, but surely has to do something with console output not getting intermingled with console input while your program is asking for input. Exactly why the InputEncoding property needs to take that lock as well is, well, a bit hard to explain but fits the pattern of serializing access to console input. This of course comes as a big surprise to many programmers, especially the ones that write little test apps that test threaded code, like you did. Bit of a setback to TDD.

The workaround is a bit unpleasant, TDD wise, you do have to stop using Console.ReadKey() to avoid the deadlock. Real programs would use the WaitOne() method of an AutoResetEvent to know that the worker thread finished executing. Or CountDownEvent.Wait(), more in keeping with trying out code a couple of times. Etcetera.


UPDATE: this deadlock scenario was resolved in a service update for .NET 4.5. Enable Windows Update on your machine to get it.

like image 175
Hans Passant Avatar answered Sep 22 '22 03:09

Hans Passant