2010-05-14 36 views
37

我想这是一个有趣的代码示例。锁定语句与Monitor.Enter方法

我们有一类 - 让我们把它测试 - 用敲定方法。在主要方法有两个代码块,我正在使用一个锁定语句和一个Monitor.Enter()调用。另外,我在这里有两个Test类的实例。 该实验非常简单:在锁定块内将测试变为空,然后尝试使用GC.Collect方法调用手动收集它。 所以,要看到敲定调用我打电话GC.WaitForPendingFinalizers方法。正如你所看到的,一切都很简单。

通过声明的定义,它是由编译器的开通试 {...} 终于 {..}块,与内部的Monitor.Enter通话尝试块和监视器。然后它在终止块中退出。我试图手动执行try-finally块。

我期望在这两种情况下都有相同的行为 - 使用锁定和使用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.最终处理类 name Test2。手动收集4. 由于test2为空引用,所以最后一个finally块中的空引用异常。

我很惊讶,并将我的代码拆卸到IL中。所以,这里是主要方法的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 

我没有看到语句和Monitor.Enter调用任何区别。 那么,为什么我仍然有的情况下的test1的实例的引用,并且对象不会被GC回收,但在使用Monitor.Enter的情况下,它被收集并敲定?

回答

18

这是因为指向test1的参考被分配给IL代码中的局部变量CS$2$0000。您清空C#中的test1变量,但lock构造以编译方式保留单独的引用。

C#编译器这样做确实很聪明。否则,可以规避lock声明在退出临界区时强制释放锁的担保。

+2

是啊。使用声明也是这样工作的。 – 2010-05-14 20:05:26

77

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

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

注意什么C#3.0规范关于这个问题说:

形式的lock语句“锁(X)......”其中x是一个引用类型的一种表达,是完全等效到

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

但x只计算一次。

这是最后一点 - 但x只计算一次 - 这是关键的行为。为了确保一次只评估一次x,就将结果存储在本地变量中,并在稍后重新使用该局部变量。

在C#4,我们已经改变了代码生成,以便它现在是

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

但同样,x是唯一评估一次。在你的程序中,你正在评估锁定表达两次。你的代码真的应该是

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中的输入是的尝试,不在外面,因为它是在C#3)

+0

为什么你决定在4.0版本中将其移入try? – 2010-05-14 20:09:23

+11

@Brian:阅读http://blogs.msdn.com/ericlippert/archive/2007/08/17/subtleties-of-c-il-codegen.aspx,然后http://blogs.msdn.com/ericlippert/ archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx – 2010-05-14 20:13:22

+0

是的,现在已经很清楚了,这是我的错,我没有看到自己的差异。感谢您的解释。 – Vokinneberg 2010-05-14 20:24:26