+1.916.577.1977 | Downloads | Buy | Register | Login
 Search  
Friday, July 25, 2008
Search Blogs
 

Available Blogs
 

Previous Blogs
 

Technorati
 
More blogs about coversant.

About Coversant
 

A question in search of a volatile answer.
 
Location: BlogsMullin' with Mullins    
Posted by: Chris Mullins 7/7/2007

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.

Permalink |  Trackback

©2008 Coversant, Inc. | Privacy Policy | About Coversant | Contact Info