The shocking truth about the C# lock statement!

In delving deep into C#, the CLR, IL and what really happens under the hood, I’ve discovered a shocking truth about the commonly used lock statement.  Everyone knows that when it’s compiled the lock statement actually uses a light weight Monitor object to handle the lock, but what you might not know (and I didn’t) was that it also throws in a try/finally in there for good measure.

Consider the following the following method that uses the lock statement:

 

                        public void UsingLock(List<string> mystuff, string value)

                        {

                                    lock (mystuff)

                                    {

                                                mystuff.Add(value);

                                    }

                        }

 

Nothing too terribly exciting, but it is common.  This method gets compiled down to the following IL:

 

.method public hidebysig instance void  UsingLock(class [mscorlib]System.Collections.Generic.List`1<string> mystuff,

                                                  string ‘value’) cil managed

{

  // Code size       25 (0x19)

  .maxstack  2

  .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<string> CS$2$0000)

  IL_0000:  ldarg.1

  IL_0001:  dup

  IL_0002:  stloc.0

  IL_0003:  call       void [mscorlib]System.Threading.Monitor::Enter(object)

  .try

  {

    IL_0008:  ldarg.1

    IL_0009:  ldarg.2

    IL_000a:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)

    IL_000f:  leave.s    IL_0018

  }  // end .try

  finally

  {

    IL_0011:  ldloc.0

    IL_0012:  call       void [mscorlib]System.Threading.Monitor::Exit(object)

    IL_0017:  endfinally

  }  // end handler

  IL_0018:  ret

} // end of method Program::UsingLock

 

You’ll notice the extra try/finally that you didn’t ask for!   Now, since the lock statement uses a Monitor object internally, you would expect the following method to compile down to the same IL:

 

                        public void UsingMonitor(List<string> mystuff, string value)

                        {

                                    Monitor.Enter(mystuff);

                                    mystuff.Add(value);

                                    Monitor.Exit(mystuff);

                        }

 

But as you’ll notice, it doesn’t!  

 

.method public hidebysig instance void  UsingMonitor(class [mscorlib]System.Collections.Generic.List`1<string> mystuff,

                                                     string ‘value’) cil managed

{

  // Code size       20 (0x14)

  .maxstack  8

  IL_0000:  ldarg.1

  IL_0001:  call       void [mscorlib]System.Threading.Monitor::Enter(object)

  IL_0006:  ldarg.1

  IL_0007:  ldarg.2

  IL_0008:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)

  IL_000d:  ldarg.1

  IL_000e:  call       void [mscorlib]System.Threading.Monitor::Exit(object)

  IL_0013:  ret

} // end of method Program::UsingMonitor

 

It results in four less IL statements than in the previous method!  Talk about a waste of cycles!   Now, just think that if this is a method that gets called relatively frequently or possibly many times at one time.   That’s a lot of waste. 

 

Now, I know that some would argue that the lock statement may be more elegant than using the Monitor object, but when it comes to scalability, common sense rules.  Use the Monitor rather than the lock whenever you can.

 

 

Happy Coding!

 

This entry was posted in Performance. Bookmark the permalink.

Leave a comment