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:
- Loading the field and put it on the stack
- Putting 1 on the stack to increment by 1
- Calling add on the stack
- 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:
- Load value from the field and put it on the stack
- 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:
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.
Post a Comment