Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lock statement vs Monitor.Enter method

I suppose that this is an interesting code example.

We have a class -- let's call it Test -- with a Finalize method. In the Main method there are two code blocks where I am using a lock statement and a Monitor.Enter() call. Also, I have two instances of the Test class here. The experiment is pretty simple: Null the Test variable within locking block and then try to collect it manually with the GC.Collect method call. So, to see the Finalize call I am calling the GC.WaitForPendingFinalizers method. Everything is very simple, as you can see.

By the definition of the lock statement, it's opened by the compiler to the try{...}finally{..} block, with a Monitor.Enter call inside of the try block and Monitor. Then it exits in the finally block. I've tried to implement the try-finally block manually.

I've expected the same behaviour in both cases -- that of using lock and that of using Monitor.Enter. But, surprise, surprise it is different, as you can see below:

public class Test {     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();     } } 

The 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 was surprised and disassembled my code into IL. So, here is the 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 don't see any difference between the lock statement and the Monitor.Enter call. So, why do I still have a reference to the instance of test1 in the case of lock, and the object is not collected by GC, but in the case of using Monitor.Enter it is collected and finalized?

like image 377
Vokinneberg Avatar asked May 14 '10 19:05

Vokinneberg


People also ask

What does lock () do in C#?

The lock statement acquires the mutual-exclusion lock for a given object, executes a statement block, and then releases the lock. While a lock is held, the thread that holds the lock can again acquire and release the lock. Any other thread is blocked from acquiring the lock and waits until the lock is released.

What is the difference between Monitor and lock?

Both Monitor and lock provides a mechanism that synchronizes access to objects. lock is the shortcut for Monitor. Enter with try and finally. Lock is a shortcut and it's the option for the basic usage.

When should I use lock C#?

C# lock in thread The lock keyword is used to get a lock for a single thread. A lock prevents several threads from accessing a resource simultaneously. Typically, you want threads to run concurrently. Using the lock in C#, we can prevent one thread from changing our code while another does so.

Is lock thread safe C#?

No this is not thread safe. To make it thread safe you can use lock on static objects because they are shared between threads, this may cause deadlocks in the code but it can be handle by maintaining proper order for locking. There is a performance cost associated with lock so use it wisely.


1 Answers

I do not see any difference between lock statement and Monitor.Enter call.

Look more carefully. The first case copies the reference to a second local variable to ensure that it stays alive.

Notice what the C# 3.0 spec says on the subject:

A lock statement of the form "lock (x) ..." where x is an expression of a reference-type, is precisely equivalent to

System.Threading.Monitor.Enter(x); try { ... } finally { System.Threading.Monitor.Exit(x); } 

except that x is only evaluated once.

It's that last bit -- except that x is only evaluated once -- that is the key to the behaviour. In order to ensure that x is evaluated only once we evaluate it once, store the result in a local variable, and re-use that local variable later.

In C# 4 we've changed the codegen so that it is now

bool entered = false; try {    System.Threading.Monitor.Enter(x, ref entered);   ...  } finally { if (entered) System.Threading.Monitor.Exit(x); } 

but again, x is only evaluated once. In your program you are evaluating the lock expression twice. Your code really should be

    bool lockTaken = false;        var temp = test2;     try {            System.Threading.Monitor.Enter(temp, ref lockTaken);            test2 = null;            Console.WriteLine("Manual collect 3.");            GC.Collect();            GC.WaitForPendingFinalizers();            Console.WriteLine("Manual collect 4.");            GC.Collect();        }        finally {           System.Threading.Monitor.Exit(temp);        }   

Now is it clear why this works the way it does?

(Also note that in C# 4 the Enter is inside the try, not outside as it was in C# 3.)

like image 130
Eric Lippert Avatar answered Oct 01 '22 01:10

Eric Lippert