Wednesday, February 11, 2009

Why ++ operator is not thread safe

Here is a quick hint on how to make your software thread safe. If you want to increment a member of your class you would probably do something like this:

public void NotSafe()
{
    val++;
}

Where val is a member of your class.But this is not thread safe. Doing this involve 4 operations:

  1. Loading the field and put it on the stack
  2. Putting 1 on the stack to increment by 1
  3. Calling add on the stack
  4. Storing the result in the field

Here is the corresponding IL:

.method public hidebysig instance void NotSafe() cil managed
{
    .maxstack 8
    L_0000: nop
    L_0001: ldarg.0
    L_0002: dup
    L_0003: ldfld int32 ClassLibrary1.Class1::val
    L_0008: ldc.i4.1
    L_0009: add
    L_000a: stfld int32 ClassLibrary1.Class1::val
    L_000f: ret
}

The problem is that anywhere between any of the 4 steps another thread can try to do the same thing. For example if a second thread pup in just after the first one is between step 1 and 2 they will both try to increment the same value and store it on the stack. To resolve this problem you can use a lock like this:

public void SafeLock()
{
    lock (valLock)
    {
        val++;
    }
}

But this will generate the following IL:

.method public hidebysig instance void SafeLock() cil managed
{
    .maxstack 3
    .locals init (
        [0] object CS$2$0000)
    L_0000: nop
    L_0001: ldarg.0
    L_0002: ldfld object ClassLibrary1.Class1::valLock
    L_0007: dup
    L_0008: stloc.0
    L_0009: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000e: nop
    L_000f: nop
    L_0010: ldarg.0
    L_0011: dup
    L_0012: ldfld int32 ClassLibrary1.Class1::val
    L_0017: ldc.i4.1
    L_0018: add
    L_0019: stfld int32 ClassLibrary1.Class1::val
    L_001e: nop
    L_001f: leave.s L_0029
    L_0021: ldloc.0
    L_0022: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_0027: nop
    L_0028: endfinally
    L_0029: nop
    L_002a: ret
    .try L_000f to L_0021 finally handler L_0021 to L_0029
}

As you can see there is a lot more code involve to ensure thread safety, A quicker, faster an easier way to do this s to use Interlocked class. This class will use low level OS call to modify the member. Here’s how to use it:

public void Safe()
{
    Interlocked.Increment(ref val);
}

This will be render as two major IL steps:

  1. Load value from the field and put it on the stack
  2. Call Increment

Here is the IL representation of this:

.method public hidebysig instance void Safe() cil managed
{
    .maxstack 8
    L_0000: nop
    L_0001: ldarg.0
    L_0002: ldflda int32 ClassLibrary1.Class1::val
    L_0007: call int32 [mscorlib]System.Threading.Interlocked::Increment(int32&)
    L_000c: pop
    L_000d: ret
}

Now every time you will see something++ you will know that this is not thread safe and how to fix it.

1 comment:

Spike said...

I think you are maybe right in your advice, but I think not really for the reasons given.
First, IL code is not executed, it is compiled into native code. And unless the CLR compiler is dumber than I think it is, it will compile a ++ of an object member into a single instruction, because the address of the object will already be in a register. Both the 32-bit and 64-bit Intel instruction sets have such an instruction, called INC. Unfortunately, despite that, INC is not guaranteed to be atomic i.e. not thread-safe on a hyperthreaded or multicore CPU.

Secondly, and it's kind of a quibble, but
Interlocked.Increment(ref val);
does not load the value of val onto the stack, it loads the *address* of val onto the stack, then the called function atomically increments the value at that address.