Kiwiana
Kiwiana

Reputation: 1033

Program hangs in release mode but works fine in debug mode

The code below works as expected in debug mode, completing after 500 milliseconds, but hangs indefinitely in release mode:

public static void Main(string[] args)
{            
   bool isComplete = false;

   var t = new Thread(() =>
   {
       int i = 0;
               
        while (!isComplete) i += 0;
   });
 
   t.Start();
    
   Thread.Sleep(500);
   isComplete = true;
   t.Join();
   Console.WriteLine("complete!");
}

I came across this and want to know the reason for this behavior in debug & release mode.

Upvotes: 112

Views: 10040

Answers (4)

Matteo Umili
Matteo Umili

Reputation: 4017

I attached to the running process and found (if I didn't make mistakes, I'm not very practiced with this) that the Thread method is translated to this:

debug051:02DE04EB loc_2DE04EB:                            
debug051:02DE04EB test    eax, eax
debug051:02DE04ED jz      short loc_2DE04EB
debug051:02DE04EF pop     ebp
debug051:02DE04F0 retn

eax (which contains the value of isComplete) is loaded first time and never refreshed.

Upvotes: 14

quetzalcoatl
quetzalcoatl

Reputation: 33506

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.

Upvotes: 151

InBetween
InBetween

Reputation: 32740

Not really an answer, but to shed some more light on the issue:

The problem seems to be when i is declared inside the lambda body and it's only read in the assignment expression. Otherwise, the code works well in release mode:

  1. i declared outside the lambda body:

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
    
  2. i is not read in the assignment expression:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { i = 0; }
    }); // Completes in release mode
    
  3. i is also read somewhere else:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { Console.WriteLine(i); i += 0; }
    }); // Completes in release mode
    

My bet is some compiler or JIT optimization regarding i is messing up things. Somebody smarter than me will probably be able to shed more light on the issue.

Nonetheless, I wouldn't worry too much about it, because I fail to see where similar code would actually serve any purpose.

Upvotes: 8

Eric Lippert
Eric Lippert

Reputation: 659956

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.

Upvotes: 83

Related Questions