This code hangs in release mode but works fine in debug mode

I guess that the optimizer is fooled by the lack of 'volatile' keyword on the isComplete variable.

Of course, you cannot add it, because it's a local variable. And of course, since it is a local variable, it should not be needed at all, because locals are kept on stack and they are naturally always "fresh".

However, after compiling, it is no longer a local variable. Since it is accessed in an anonymous delegate, the code is split, and it is translated into a helper class and member field, something like:

public static void Main(string[] args)
{
    TheHelper hlp = new TheHelper();

    var t = new Thread(hlp.Body);

    t.Start();

    Thread.Sleep(500);
    hlp.isComplete = true;
    t.Join();
    Console.WriteLine("complete!");
}

private class TheHelper
{
    public bool isComplete = false;

    public void Body()
    {
        int i = 0;

        while (!isComplete) i += 0;
    }
}

I can imagine now that the JIT compiler/optimizer in a multithreaded environment, when processing TheHelper class, can actually cache the value false in some register or stack frame at the start of the Body() method, and never refresh it until the method ends. That's because there is NO GUARANTEE that the thread&method will NOT end before the "=true" gets executed, so if there is no guarantee, then why not cache it and get the performance boost of reading the heap object once instead of reading it at every iteration.

This is exactly why the keyword volatile exists.

For this helper-class to be correct a tiny little bit better 1) in multi-threaded environments, it should have:

    public volatile bool isComplete = false;

but, of course, since it's autogenerated code, you can't add it. A better approach would be to add some lock()s around reads and writes to isCompleted, or to use some other ready-to-use synchronization or threading/tasking utilities instead of trying to do it bare-metal (which it will not be bare-metal, since it's C# on CLR with GC, JIT and (..)).

The difference in debug mode occurs probably because in debug mode many optimisations are excluded, so you can, well, debug the code you see on the screen. Therefore while (!isComplete) is not optimized so you can set a breakpoint there, and therefore isComplete is not aggressively cached in a register or stack at the method start and is read from the object on the heap at every loop iteration.

BTW. That's just my guesses on that. I didn't even try to compile it.

BTW. It doesn't seem to be a bug; it's more like a very obscure side effect. Also, if I'm right about it, then it may be a language deficiency - C# should allow to place 'volatile' keyword on local variables that are captured and promoted to member fields in the closures.

1) see below for a comments from Eric Lippert about volatile and/or this very interesting article showing the levels of complexity involved in ensuring that code relying on volatile is safe ..uh, good ..uh, let's say OK.


The answer of quetzalcoatl is correct. To shed more light on it:

The C# compiler and CLR jitter are permitted to make a great many optimizations that assume that the current thread is the only thread running. If those optimizations make the program incorrect in a world where the current thread is not the only thread running that is your problem. You are required to write multithreaded programs that tell the compiler and jitter what crazy multithreaded stuff you are doing.

In this particular case the jitter is permitted -- but not required -- to observe that the variable is unchanged by the loop body and to therefore conclude that -- since by assumption this is the only thread running -- the variable will never change. If it never changes then the variable needs to be checked for truth once, not every time through the loop. And this is in fact what is happening.

How to solve this? Don't write multithreaded programs. Multithreading is incredibly hard to get right, even for experts. If you must, then use the highest level mechanisms to achieve your goal. The solution here is not to make the variable volatile. The solution here is to write a cancellable task and use the Task Parallel Library cancellation mechanism. Let the TPL worry about getting the threading logic right and the cancellation properly send across threads.