Subterranean IL: Volatile

Posted by Simon Cooper on Simple Talk See other posts from Simple Talk or by Simon Cooper
Published on Wed, 24 Nov 2010 13:22:00 GMT Indexed on 2010/12/06 16:58 UTC
Read the original article Hit count: 329

Filed under:

This time, we'll be having a look at the volatile. prefix instruction, and one of the differences between volatile in IL and C#.

The volatile. prefix

volatile is a tricky one, as there's varying levels of documentation on it. From what I can see, it has two effects:

  1. It prevents caching of the load or store value; rather than reading or writing to a cached version of the memory location (say, the processor register or cache), it forces the value to be loaded or stored at the 'actual' memory location, so it is then immediately visible to other threads.
  2. It forces a memory barrier at the prefixed instruction. This ensures instructions don't get re-ordered around the volatile instruction. This is slightly more complicated than it first seems, and only seems to matter on certain architectures. For more details, Joe Duffy has a blog post going into the details.
For this post, I'll be concentrating on the first aspect of volatile.

Caching field accesses

To demonstrate this, I created a simple multithreaded IL program. It boils down to the following code:

.class public Holder {
.field public static class Holder holder
.field public bool stop

.method public static specialname void .cctor() {
newobj instance void Holder::.ctor()
stsfld class Holder Holder::holder
ret
}
}

.method private static void Main() {
.entrypoint
// Thread t = new Thread(new ThreadStart(DoWork))
// t.Start()
// Thread.Sleep(2000)
// Console.WriteLine("Stopping thread...")

ldsfld class Holder Holder::holder
ldc.i4.1
stfld bool Holder::stop

call instance void [mscorlib]System.Threading.Thread::Join()

ret
}

.method private static void DoWork() {
ldsfld class Holder Holder::holder

// while (!Holder.holder.stop) {}
DoWork:
dup
ldfld bool Holder::stop
brfalse DoWork

pop
ret
}
If you compile and run this code, you'll find that the call to Thread.Join() never returns - the DoWork spinlock is reading a cached version of Holder.stop, which is never being updated with the new value set by the Main method. Adding volatile to the ldfld fixes this:
dup
volatile.
ldfld bool Holder::stop
brfalse DoWork

The volatile ldfld forces the field access to read direct from heap memory, which is then updated by the main thread, rather than using a cached copy.

volatile in C#

This highlights one of the differences between IL and C#. In IL, volatile only applies to the prefixed instruction, whereas in C#, volatile is specified on a field to indicate that all accesses to that field should be volatile (interestingly, there's no mention of the 'no caching' aspect of volatile in the C# spec; it only focuses on the memory barrier aspect). Furthermore, this information needs to be stored within the assembly somehow, as such a field might be accessed directly from outside the assembly, but there's no concept of a 'volatile field' in IL! How this information is stored with the field will be the subject of my next post.

© Simple Talk or respective owner