4

I have following code:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Started");
        var res = GetSlowStringAsync();

        res.ContinueWith(
            c =>
                {
                    Console.WriteLine("Will it crash?");
                    //Console.WriteLine(c.Result);
                }
            );

        //Console.WriteLine("Press any key");
        Console.ReadKey();
        Console.WriteLine("Continue on the main thread");

        GC.Collect();
        Console.WriteLine("Memory cleared");

        Thread.Sleep(10000);
    }

    public static async Task<string> GetSlowStringAsync()
    {
        await Task.Delay(2000);
        throw new Exception("will you handle it?");
        return "somestring";
    }
}

also in App.config I added following lines:

<runtime>
    <ThrowUnobservedTaskExceptions enabled ="true" />
</runtime>

I'm using visual studio 2015 14.0.23107.0 D14REL Target .Net Framework 4.6. Execute in "Start without debugging" mode.

If after question "Will it crash" press any button, then program will crash.

But if to uncomment

Console.WriteLine("Press any key");

and also execute in "Run without debugging" mode then program will not crash. Why Console.WriteLine affects way of raising exceptions?

Yuriy Zaletskyy
  • 4,983
  • 5
  • 34
  • 54

1 Answers1

3

I very much doubt that anybody can repro the problem from your snippet. My crystal ball says that you made a late change the program, adding the Thread.Sleep() at the end. And hid the threading-race bug that the original program had. I'll write this answer assuming that call isn't there.

The exception is only thrown when all of these conditions are true:

  • You run the Release build of the program
  • You run the program without a debugger attached
  • The GC.Collect() call actually garbage-collects the res object. That will not be the case if the previous two conditions are not met
  • The finalizer thread has enough time to do its job after the GC.Collect() finishes before the main() method ends. That Console.WriteLine() call can give it just enough time to run the TaskExceptionHolder finalizer. Not a guarantee, the problem with threading-race bugs.

Ways to change the outcome are Tools > Options > Debugging > General > untick the "Suppress JIT optimization" option. That ensures that the outcome is not affect by having a debugger attached. And adding res = null; before the GC.Collect() call or moving the code into another method, that ensures that res can be collected even in the Debug build. Why GC.Collect() behaves so unpredictably is explained in this post.

The only other thing you'd need to know to make sense of the behavior is that the exception is suppressed when the program is busy shutting down. Note the use of the Environment.HasShutdownStarted and AppDomain.CurrentDomain.IsFinalizingForUnload properties in the TaskExceptionHolder finalizer. So if the finalizer thread is a bit slow at doing its job then shutdown might have already started by the time the finalizer runs and you won't get the exception. Anything you add to Main() after the GC.Collect() call, like Console.WriteLine(), improves the odds that you see the exception.

Adding GC.WaitForPendingFinalizers() solves the threading-race bug and makes the outcome predictable. Well, more predictable :)

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • I reproduced problem at VS2012 also. Also @Abhishek today also reproduced it. So there are people which reproduced it. Maybe we can double check in chat? – Yuriy Zaletskyy Sep 18 '15 at 15:15
  • I don't have it installed anymore. Chasing threading-race bugs is a pretty fruitless endeavor anyway, they never repeat well from one machine to another. Just fix the bug. – Hans Passant Sep 18 '15 at 15:22