Lock语句与Monitor.Enter方法

时间:2010-05-14 19:39:58

标签: c# command-line-interface garbage-collection

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

我们有一个类 - 我们称之为测试 - 使用 Finalize 方法。在 Main 方法中,有两个代码块,我使用的是lock语句和 Monitor.Enter()调用。另外,我在这里有两个测试类的实例。 实验非常简单:在锁定块中取消测试变量,然后尝试使用 GC.Collect 方法调用手动收集它。 因此,要查看 Finalize 调用,我将调用 GC.WaitForPendingFinalizers 方法。正如你所看到的,一切都很简单。

根据 lock 语句的定义,编译器将其打开到尝试 {...} 最终 {..}阻止,在尝试块内部进行 Monitor.Enter 调用,并使用监控。然后它退出 finally 块。我试图手动实现 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.完成课程   名称Test2。手动收集4。   并且最后一个块中的null引用异常,因为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

我认为 lock 语句与 Monitor.Enter 调用之间没有任何区别。 那么,为什么我仍然在 lock 的情况下引用 test1 的实例,并且该对象不是由GC收集的,而是在使用<的情况下strong> Monitor.Enter 它被收集并最终确定?

2 个答案:

答案 0 :(得分:80)

  

我没有看到lock语句和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中,我们已经更改了codegen,现在它已经

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之外。)

答案 1 :(得分:20)

这是因为test1指向的引用被分配给IL代码中的局部变量CS$2$0000。您在C#中清空了test1变量,但lock构造的编译方式使得维护单独的引用。

C#编译器实际上非常聪明。否则,有可能绕过保证lock语句应该在退出临界区时强制释放锁。