Imagine you start a thread to perform some long-running task that you want the ability to cancel. For example, you might start that thread while your main program is waiting on user input, and then cancel that thread once the user has pressed Enter. It’s an easy thing to code up.
private static void Main(string[] args)
{
bool cancelRequested = false;
Console.WriteLine("Starting thread.");
ThreadPool.QueueUserWorkItem((s) =>
{
Console.WriteLine("Thread started.");
int counter = 0;
while (!cancelRequested)
{
// do some work here
++counter;
}
Console.WriteLine("Thread stopped: {0}", counter);
});
Console.WriteLine("Press Enter to stop thread.");
Console.ReadLine();
cancelRequested = true;
Console.WriteLine("Press Enter again to exit the program: ");
Console.ReadLine();
}
If you run that program under the debugger, you’ll find that it works just fine in Debug and in Release mode. When you press Enter, you’ll see the “Thread stopped” message as expected. However, if you run without the debugger attached, you’ll find that the thread never terminates! What’s going on?
Simply put, compiler optimization. The JIT compiler sees that nothing in the loop can modify the cancelRequested
variable, so it generates code to load the value and keep it in a register. The thread never references that variable again.
Interestingly, placing a Thread.Sleep(0)
in the loop disables that optimization, as does adding a lock
statement. Other function calls inside the loop might also prevent that optimization, as will sufficiently complex calculations. If the exact rules for what prevents that optimization are written anywhere, I’ve been unable to find them. The C# Language Specification hints at some things, but I was unable to find any explicit language.
Absent explicit language in the specification, I’m hesitant to rely on the current behavior because it could change in the next version of the compiler or on a different platform.
There are various ways to force the compiler to examine memory at every iteration of the loop, but the options are limited when working with a Boolean
. Interlocked.CompareExchange
came immediately to mind, except that there isn’t an overload that takes a ref bool
. I could make the variable an int
, and write:
while (Interlocked.CompareExchange(ref cancelRequested, 1, 1) == 0)
Besides being ugly, it’s not at all clear what’s going on there. I’ve written stuff like that and six months later had to come back and figure out what’s happening. If you had to go look up Interlocked.CompareExchange and puzzle out what’s happening, I think you’ll agree that this is a less than ideal solution.
Those of you with some more experience might say, “Make it volatile!” There are some problems with that. First, you can’t mark a local variable with the volatile keyword. I’d have to promote the variable to class scope. That works, sure, but it’s dissatisfying having to move a variable to an outer scope just so I can make it work the way I want. It just feels wrong.
A major problem with volatile
is that it applies different semantics to variable access, but does it at the declaration site. I don’t know, when I’m writing code to access a variable, that doing so will involve acquire semantics. It would be like stepping back to the bad old days of Pascal, where passing a reference (var
) parameter looked the same as passing a parameter by value. That is, given this procedure:
procedure Frob(var fooby: Integer);
The code to call it doesn’t explicitly state that the parameter is passed by reference:
Frob(foo);
I rather like having to specify ref
in C#, and I don’t like the implicit change in memory access semantics that comes along with volatile
.
At least Interlocked.CompareExchange
tells you that there’s something special about that cancelRequested
variable, even if the logic behind what’s happening is confusing.
More importantly, people who know a lot more than I do about C#, .NET, and multithreading in general caution against the use of volatile
for several reasons, one of which is that it probably doesn’t work the way you think it works or do what you think it does. See, for example, Eric Lippert’s Atomicity, volatility and immutability are different, part three and Joe Duffy’s Sayonara volatile.
Based on those comments, volatile
is not the thing I want to use when trying to write portable and reliable code. I would suggest that you, too, avoid it.
All that said, there are other ways I could solve this problem. But the simple program I showed at the start of this entry is a pretty rare case. Real programs often have multiple background tasks running, all of which are subject to cancellation either individually or as a group, and a simple Boolean
variable is not a very good way to control such things.
There is a much more flexible and reliable way, which I’ll talk about in my next post.