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 (4)

  1. Peter Ritchie says:

    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.

  2. irenak says:

    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.

  3. 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…

  4. irenak says:

    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.