Lock statement vs Monitor.Enter method.
- by Vokinneberg
I suppose it is an interesting code example.
We have a class, let's call it Test with Finalize method. In Main method here is two code blocks where i am using lock statement and Monitor.Enter call. Also i have two instances of class Test here.
The experiment is pretty simple - nulling Test variable within locking block and try to collect it manually with GC.Collect method call.
So, to see the Finilaze call i am calling GC.WaitForPendingFinalizers method. Everything is very simple as you can see.
By defenition of lock statement it's opens by compiler to try{...}finally{..} block with Minitor.Enter call inside of try block and Monitor.Exit in finally block. I've tryed to implement try-finally block manually.
I've expected the same behaviour in both cases. in case of using lock and in case of unsing Monitor.Enter. But, surprize, surprize - it is different as you can see below.
public class Test : IDisposable
{
private string name;
public Test(string name)
{
this.name = name;
}
~Test()
{
Console.WriteLine(string.Format("Finalizing class name {0}.", name));
}
}
class Program
{
static void Main(string[] args)
{
var test1 = new Test("Test1");
var test2 = new Test("Tesst2");
lock (test1)
{
test1 = null;
Console.WriteLine("Manual collect 1.");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Manual collect 2.");
GC.Collect();
}
var lockTaken = false;
System.Threading.Monitor.Enter(test2, ref lockTaken);
try {
test2 = null;
Console.WriteLine("Manual collect 3.");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Manual collect 4.");
GC.Collect();
}
finally {
System.Threading.Monitor.Exit(test2);
}
Console.ReadLine();
}
}
Output of this example is
Manual collect 1. Manual collect 2.
Manual collect 3. Finalizing class
name Test2. Manual collect 4.
And null reference exception in last finally block because test2 is null reference.
I've was surprised and disasembly my code into IL. So, here is IL dump of Main method.
.entrypoint
.maxstack 2
.locals init (
[0] class ConsoleApplication2.Test test1,
[1] class ConsoleApplication2.Test test2,
[2] bool lockTaken,
[3] bool <>s__LockTaken0,
[4] class ConsoleApplication2.Test CS$2$0000,
[5] bool CS$4$0001)
L_0000: nop
L_0001: ldstr "Test1"
L_0006: newobj instance void ConsoleApplication2.Test::.ctor(string)
L_000b: stloc.0
L_000c: ldstr "Tesst2"
L_0011: newobj instance void ConsoleApplication2.Test::.ctor(string)
L_0016: stloc.1
L_0017: ldc.i4.0
L_0018: stloc.3
L_0019: ldloc.0
L_001a: dup
L_001b: stloc.s CS$2$0000
L_001d: ldloca.s <>s__LockTaken0
L_001f: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&)
L_0024: nop
L_0025: nop
L_0026: ldnull
L_0027: stloc.0
L_0028: ldstr "Manual collect."
L_002d: call void [mscorlib]System.Console::WriteLine(string)
L_0032: nop
L_0033: call void [mscorlib]System.GC::Collect()
L_0038: nop
L_0039: call void [mscorlib]System.GC::WaitForPendingFinalizers()
L_003e: nop
L_003f: ldstr "Manual collect."
L_0044: call void [mscorlib]System.Console::WriteLine(string)
L_0049: nop
L_004a: call void [mscorlib]System.GC::Collect()
L_004f: nop
L_0050: nop
L_0051: leave.s L_0066
L_0053: ldloc.3
L_0054: ldc.i4.0
L_0055: ceq
L_0057: stloc.s CS$4$0001
L_0059: ldloc.s CS$4$0001
L_005b: brtrue.s L_0065
L_005d: ldloc.s CS$2$0000
L_005f: call void [mscorlib]System.Threading.Monitor::Exit(object)
L_0064: nop
L_0065: endfinally
L_0066: nop
L_0067: ldc.i4.0
L_0068: stloc.2
L_0069: ldloc.1
L_006a: ldloca.s lockTaken
L_006c: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&)
L_0071: nop
L_0072: nop
L_0073: ldnull
L_0074: stloc.1
L_0075: ldstr "Manual collect."
L_007a: call void [mscorlib]System.Console::WriteLine(string)
L_007f: nop
L_0080: call void [mscorlib]System.GC::Collect()
L_0085: nop
L_0086: call void [mscorlib]System.GC::WaitForPendingFinalizers()
L_008b: nop
L_008c: ldstr "Manual collect."
L_0091: call void [mscorlib]System.Console::WriteLine(string)
L_0096: nop
L_0097: call void [mscorlib]System.GC::Collect()
L_009c: nop
L_009d: nop
L_009e: leave.s L_00aa
L_00a0: nop
L_00a1: ldloc.1
L_00a2: call void [mscorlib]System.Threading.Monitor::Exit(object)
L_00a7: nop
L_00a8: nop
L_00a9: endfinally
L_00aa: nop
L_00ab: call string [mscorlib]System.Console::ReadLine()
L_00b0: pop
L_00b1: ret
.try L_0019 to L_0053 finally handler L_0053 to L_0066
.try L_0072 to L_00a0 finally handler L_00a0 to L_00aa
I does not see any difference between lock statement and Monitor.Enter call.
So, why i steel have a reference to the instance of test1 in case of lock, and object is not collected by GC, but in case of using Monitor.Enter it is collected and finilized?