SYSK 213: Curious to Know How C# lock Keyword is Actually Implemented? Then read on…
We know that C# keywords are simply programming language (in this case, C#) lingo that map into the .NET framework types, objects, etc. So, what does the ‘lock’ keyword map to? Through the simple use of ildasm tool, you can see that the lock keyword ends up being a call to System.Threading.Monitor.Enter/Exit. Consider the following simple code snippet:
object someObject = new object();
. . .
private void button1_Click(object sender, EventArgs e)
{
lock(someObject)
{
// Your code here
}
}
In IL, it’s represented by the following instruction set:
.method private hidebysig instance void button1_Click(object sender, class [mscorlib]System.EventArgs e) cil managed
{
// Code size 29 (0x1d)
.maxstack 2
.locals init ([0] object CS$2$0000)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld object WindowsApplication1.Form1::someObject
IL_0007: dup
IL_0008: stloc.0
IL_0009: call void [mscorlib]System.Threading.Monitor::Enter(object)
IL_000e: nop
.try
{
IL_000f: nop
IL_0010: nop
IL_0011: leave.s IL_001b
} // end .try
finally
{
IL_0013: ldloc.0
IL_0014: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_0019: nop
IL_001a: endfinally
} // end handler
IL_001b: nop
IL_001c: ret
} // end of method Form1::button1_Click
Now consider the following:
private void button1_Click(object sender, EventArgs e)
{
System.Threading.Monitor.Enter(someObject);
try
{
// Your code here
}
finally
{
System.Threading.Monitor.Exit(someObject);
}
}
The resulting IL is almost identical to the one produced by the lock keyword:
.method private hidebysig instance void button1_Click(object sender, class [mscorlib]System.EventArgs e) cil managed
{
// Code size 34 (0x22)
.maxstack 1
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld object WindowsApplication1.Form1::someObject
IL_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
IL_000c: nop
.try
{
IL_000d: nop
IL_000e: nop
IL_000f: leave.s IL_0020
} // end .try
finally
{
IL_0011: nop
IL_0012: ldarg.0
IL_0013: ldfld object WindowsApplication1.Form1::someObject
IL_0018: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_001d: nop
IL_001e: nop
IL_001f: endfinally
} // end handler
IL_0020: nop
IL_0021: ret
} // end of method Form1::button1_Click
Comments
Anonymous
October 06, 2006
Some of your more observant readers will notice that Monitor.Enter(Object) is used, and not Monitor.TryEnter(Object, TimeSpan) (or similar). Which means, the lock keyword has no timeout and has the potential for deadlock. If you want to timeout on the lock you have to explicitly use Monitor.TryEnter instead of lock.Anonymous
October 06, 2006
I agree with you that the best practice is to use Monitor.TryEnter, so your code has a chance to handle deadlock conditions. But this post was merely an attempt to look undercover of the lock keyword, which, from what I can see, is using Monitor.Enter, not TryEnter.Anonymous
October 06, 2006
It's just mean to say "almost identical" without calling out what the differences actually are ;) (and why, presumably, they're irrelevant).Also, unrelated to your point but I just noticed it and I'm curious. Why do both versions produce so many nops in the resulting IL? I always assumed nops were a rarity - only needed if for example you needed to jump to a point where there's no actual code. But in this case there seems to be an almost 50/50 nop-to-code ratio...Anonymous
October 06, 2006
The reason for such large percentage of NOPs in the code above is because the source code has no real logic, and has branches (if statements that result in a jump instruction). To my knowledge, if your code has no effect, it results in NOP. Also, NOPs are most commonly used for timing purposes, to force memory alignment, to prevent instructions on pipelined processor from being processed in the wrong order, to name a few. With branching, the processor cannot tell in advance whether it should process the next instruction or not... thus NOP. On Intel x86 CPU family are, the NOP results in 3 clock cycles.