locking语句与Monitor.Enter方法

我想这是一个有趣的代码示例。

我们有一个类 – 我们称之为testing – 用Finalize方法。 在Main方法中有两个代码块,我正在使用一个locking语句和一个Monitor.Enter()调用。 另外,我在这里有两个Test类的实例。 实验非常简单:在locking块中将Testvariables置零,然后尝试使用GC.Collect方法调用手动收集它。 所以,要看到Finalize调用,我打电话给GC.WaitForPendingFinalizers方法。 如你所见,一切都很简单。

通过locking语句的定义,编译器将其打开到try {…} finally {..}块,并在try块和Monitor中调用Monitor.Enter 。 然后它在finally块中退出。 我试图手动实现try-finally块。

我期望在这两种情况下都有相同的行为 – 使用locking和使用Monitor.Enter 。 但是,令人惊讶的是,它是不同的,如下所示:

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(); } } 

这个例子的输出是:

手工收集1.手动收集2.手动收集3.确定class级名称Test2。 手动收集4.最后finally块中的空引用exception,因为test2是空引用。

我很惊讶,把我的代码拆分成IL。 所以,这里是Main方法的IL转储:

 .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 

我没有看到locking语句和Monitor.Enter调用之间的任何区别。 那么,为什么在的情况下我仍然有一个对test1实例的引用,并且这个对象不是由GC收集的,但是在使用Monitor.Enter的情况下,它是被收集和完成的?

这是因为test1指向的引用被分配给IL代码中的局部variablesCS$2$0000 。 您在C#中将test1variablestest1 ,但lock构造被编译为保持单独引用的方式。

C#编译器这样做确实很聪明。 否则,可以规避lock语句应该在退出临界区域时解除locking的担保。

我没有看到locking语句和Monitor.Enter调用之间的任何区别。

仔细看看。 第一个案例将引用复制到第二个局部variables以确保它保持活动状态。

注意C#3.0规范在这个主题上的说明:

forms为“lock(x)…”的locking语句(其中x是引用types的expression式)恰恰等同于

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

除了x只被评估一次。

这是最后一点 – 除了x只评估一次 – 这是行为的关键。 为了确保x只被评估一次,我们只评估一次,将结果存储在一个局部variables中,稍后重新使用这个局部variables。

在C#4中,我们已经改变了Codegen,现在是这样

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

但是再一次,x只被评估一次。 在你的程序中,你正在计算两次lockingexpression式。 你的代码真的应该是

  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); } 

现在很清楚为什么这个工作的方式呢?

(还要注意,在C#4中,Enter是 try中,而不是在C#3中。)