Lock语句与Monitor.Enter方法

Vok*_*erg 41 c# garbage-collection command-line-interface

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

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

通过lock语句的定义,编译器将其打开到try {...} finally {..}块,并在try块和Monitor内部调用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这个例子的输出是:

手动收集1.手动收集2.手动收集3.完成类名Test2.手动收集4.并且最后一个块中的null引用异常,因为test2是空引用.

我很惊讶并将我的代码拆解成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
Run Code Online (Sandbox Code Playgroud)

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

Eri*_*ert 81

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

仔细看看.第一种情况将引用复制到第二个局部变量以确保它保持活动状态.

请注意C#3.0规范对此主题的说法:

形式为"lock(x)..."的锁定语句,其中x是引用类型的表达式,精确等效于

System.Threading.Monitor.Enter(x);
try { ... }
finally { System.Threading.Monitor.Exit(x); }
Run Code Online (Sandbox Code Playgroud)

除了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); }
Run Code Online (Sandbox Code Playgroud)

但同样,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);   
    }  
Run Code Online (Sandbox Code Playgroud)

现在很明显为什么它的工作方式呢?

(另请注意,在C#4中,Enter try中,而不是在C#3中.)

  • @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 (14认同)

Bri*_*eon 21

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

C#编译器实际上非常聪明.否则,有可能绕过保证人,该lock声明应该在退出关键部分时强制释放锁定.

  • 对.using语句也是这样工作的. (3认同)