LA.NET [EN]

Jul 02

As we’ve seen in the previous post, most processors give us important insurances regarding memory loads and stores. However, even though those insurances are important and can be used in several scenarios, the truth is that they aren’t enough for all real world tasks.

Fortunately, most processors also offer a group of interlocked operations which enable atomic compare and swap scenarios. These operations rely on hardware and interprocess synchronization. Notice that these operations aren’t as simply as they might seem at first sight. For instance, it’s important to recall that in today’s architectures, these kind of operations need to play well with caches. My point is that event though these operations tend to be cheaper that the traditional locks, they’re still not cheap (for instance, there are cases where an interlocked operation ends up locking the bus and that is not a good thing).

Currently, there are several kinds of interlocked operations. In .NET, all interlocked operations are exposed as members of the Interlocked class:

  • Add: adds two integers (int or long) and replaces the first with the value of the sum;
  • CompareAndExchange: compares two values and if they’re equal, replaces one of them with the other (notice that this method receives three parameters). This is probably the most important method of this class (it supports compares and exchanges on reference types two!);
  • Decrement: Similar to add,but in this case,it performs a subtraction;
  • Exchange: updates a variable with another value;
  • Increment: adds one to an existing variable;
  • Read: reads a value from a variable;

As I’ve said before, the advantage of using these methods is that all the operations are performed atomically. The Read method might not seen necessary at first until you look at its signature:

public static long Read(ref long location);

As you can see, it should only be used in 32 bits system when you need to access 64 bits integers. Atomically setting a value on 64 bits can be done through the Exchange method:

public static extern long Exchange(ref long location1, long value);

This means that you can easily build a generic write or read routine and reuse them across your programs:

public static class Generic64Helper {
  private const int WordSize32 = 4;
  public static void WriteLong(ref long location, long value) {
    if (IntPtr.Size == WordSize32) {
        Interlocked.Exchange(ref location, value);
    } else {
        location = value;
    }
  }
  public static long ReadLong(ref long location) {
    if (IntPtr.Size == WordSize32) {
        return Interlocked.Read(ref location);
    }
    return location;
  }
}

There a couple of interesting things going on here. First, we use the IntPtr.Size property to get the size of the “current” word. In 64 bits, we really don’t want to pay the price of the Interlocked operation and will simply delegate to a hardware atomic write or read. However, in 32 bits system (where the word size is 4), we really need to use those methods to have atomicity. Reusing this class is rather simple, as you can see from the next snippet (t is initialized with 10 and y is initialized with the value of t):

Generic64Helper.WriteLong(ref t, 10);
long y = Generic64Helper.ReadLong(ref t);

There a couple of gotchas with the previous approach (notice that only the read and write are atomics), but we’ll leave that for a future post. Now, the important thing is to concentrate on introducing most of these methods. If you use reflector, you should see that Interlocked.Read depends directly on the CompareExchange method:

public static long Read(ref long location) {
  return CompareExchange(ref location, 0L, 0L);
}

This is really a cool trick!

As I’ve said before, the CompareExchange method is one of the most important methods exposed by the class. The CompareExchange method receives three parameters: if the first and third parameters are equal, then it updates the first with the value of the second parameter. The method will always return the initial value of the first parameter (even when there’s no update).

So, the code used on the Read method will only end up doing a write (ie, a store) if the current stored value is 0. And even when this happens, there really isn’t any update in the value (notice that if its value is 0 we’re writing 0 again!).

The remaining methods exposed by the class are fairly obvious, so I won’t really go into examples here (the docs do a good job on showing how to use these methods). The important thing to take from this post is that these operations are atomic and are performed at the hardware level. On the next post, we’ll keep talking about this class and on how you can reuse them to add some free lock programming to your apps. Keep tuned!

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>