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!