A Real Volatile Problem
Everybody who get into doing any serious concurrency work, quickly
comes across Volatile variables.
A few of us are even lucky enough to have someone smart around to actually explain the issue. When I
first came across volatile variables, Jeff Brady
was kind enough to give me a good explanation of compiler optimization, and
how things could be different between Debug and Release builds.
This blog is aimed at people new to concurrency, or those on the
mentoring side looking for a good example to give the threading novice. For years,
I've known about, and occasionally came across a real problems due to volatile
variables. But in all this time, I never bothered to put together a real example
that cleanly demonstrated the issue. The time has finally come!
Volatile variables, are something those developers new to concurrency don't
think about. Especially when it's not obvious their code is concurrent - as
is often the case with Timer Callbacks, ASP.Net pages, Async I/O,
Delegate.BeginInvoke, or other related technologies.
Jon Skeet has written
a nice primer on Volatile Variables,
and there is also a lot of material online regarding volatility. If you're not sure what a volatile variable is,
and the related data caching issues, I'll wait while you go hit Google.
The real problem here is that caching related problems are really hard to debug!
As an example, I offer the following code:
static void Main(string[] args)
{
Thread t1 = new Thread(VolatileLoop.ThreadOne);
Thread t2 = new Thread(VolatileLoop.ThreadTwo);
t1.Start();
Thread.Sleep(1000);
t2.Start();
Console.ReadLine();
}
public static class VolatileLoop
{
private static bool shouldStop = false;
public static void ThreadTwo()
{
shouldStop = true;
}
public static void ThreadOne()
{
int counter = 0;
while (shouldStop == false)
counter++;
Console.WriteLine(counter);
}
}
Take a moment to look at this code, and figure out what it's going to do.
Now take a moment to put it into Visual Studio, and run it.
... it ran fine right? Right. The loop exited, and everything is good. Now build it in release mode, and run it again.
... it still ran fine, right? Right.The loop again exited, and things are good.
... now run it from outside Visual Studio (or "Start Without Debugging.").
... and notice that it's stuck in an infinite loop!
This behavior is what really drives many developers nuts. The behavior of the jitter is different with a debugger attached. You can
change this setting, but few people know it's there.
Let's rehash our example:
- It runs just fine when built in Debug mode and run under the Debugger.
- It runs just fine when built in Debug mode, and run normally.
- It runs just fine when built in release mode, and run under the Debugger.
- It fails to run properly when built in release mode, and run normally.
Some Possible Solutions
How many of you, in order to debug this, would try:
while (shouldStop == false)
{
counter++;
Console.WriteLine(counter);
}
The problem with this, is that the act of putting in the Console.WriteLine make the code always work! Of course, this means it now works, so you're done, right?
There are a number of ways to fix this application, some better than others.
Almost-Pattern: Use a Volatile variable
The obvious solution is to change the definition of shouldStop to be volatile:
private static volatile bool shouldStop = false;
I personally am not a fan of volatile variables. There are a lot of subtle issues involved with them,
and they often lead to long term maintenance issues. I've spent days debating
with experts over the interaction between volatile variables (and if these guys can't agree, what chance do us mortals have?). Many of these debates end up
with 'Email Duffy. Hopefully he can clear this up.'. Even then, sometimes, he's not sure.
In many cases, volatile variables are far trickier than they appear, and their interaction with the .Net Memory Model is anything but obvious.
Anti-Pattern: Use a Memory Barrier
Another solution is to throw a MemoryBarrier in there:
while (shouldStop == false)
{
Thread.MemoryBarrier();
counter++;
}
Using the memory barrier really isn't the way to go - it'll
work, but unless you're one of the very, very select few who
really understand the .Net Memory Model
(e.g. Your last name is Richter,
Duffy, or Morrison)
then you should avoid this solution.
Anti-Pattern: Use a Thread.VolatileRead
Another semi-common solution is to use Thread.VolatileRead. This one
should also be avoided, as it's memory model rules are much more drastic than a standard volatile variable.
Pattern: Use standard locking mechanisms
The BEST answer, bar none, is to punt on the entire volatile infrastructure and use Lock/Synclock.
I like to do this with a property and a C# lock. The code for taking this approach looks like:
private static bool _shouldStop = false;
private static object _syncRoot = new object();
public static bool ShouldStop
{
get
{
lock (_syncRoot)
{
return _shouldStop;
}
}
set
{
lock (_syncRoot)
{
_shouldStop = value;
}
}
}
This approach has the advantages of being less bug prone, easier to maintain, and much clearer than something threading related it going on. In my opinion,
any argument as to the lesser performance of this approach need to be justified by a Profiler (Ants,
DotTrace, etc) before you should even think of moving from this approach to one using volatile variables.
Summary
I put forward here an example that works in all the common debugging scenarios, but fails very
consistantly under 'real' usage. Attempts to debug the problem by either running under the debugger,
or by adding in debug code, causes the problem to disappear.
These bugs are a pain the neck to track down, but hopefully after reading this you'll be a bit more aware of volatility and the related caching issues.