Reputation: 1033
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
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
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
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:
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
i
is not read in the assignment expression:
var t = new Thread(() =>
{
int i = 0;
while (!isComplete) { i = 0; }
}); // Completes in release mode
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
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