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