锁语句 vs Monitor.Enter方法

47

我认为这个代码示例非常有趣。

我们有一个类——我们称之为Test——有一个Finalize方法。在Main方法中,有两个代码块,其中一个使用了lock语句,另一个使用了Monitor.Enter()方法调用。此外,我这里有两个Test类的实例。

这个实验非常简单:在锁定块内将Test变量置为null,然后尝试手动收集它并调用GC.Collect方法。

所以,为了看到Finalize调用,我调用GC.WaitForPendingFinalizers方法。你可以看到,一切都非常简单。

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

我期望在使用lock和使用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。最后一个 finally 块中出现了空引用异常,因为 test2 是空引用。

我感到惊讶并分解了我的代码成为 IL(Intermediate Language)。因此,这里是 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实例有一个引用,而且对象没有被垃圾回收,但是在使用Monitor.Enter时,它会被回收和完成?

2个回答

83

我认为lock语句和Monitor.Enter调用没有任何区别。

请仔细观察。在第一种情况下,将引用复制到第二个局部变量中以确保它的生命周期。

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

形如“lock(x) …”的锁语句(其中x是引用类型的表达式)与Monitor.Enter调用完全等效。

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中,Enter位于try内部,而不像在C# 3中那样在外部。)


你为什么决定在4.0中将它移动到try块内部? - Brian Gideon
14
@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。 - Eric Lippert
是的,现在很清楚了,我的错我没有自己看到区别。感谢您的解释。 - Vokinneberg
1
我想知道.NET是否会提供创建IDisposable对象的等效方法(例如,让正在构建的对象将自身存储在byref参数中,以便如果构造函数引发异常,则可以处理部分构建的对象(当然,Dispose必须知道对象可能没有完全构建,但在许多情况下,这应该不太难)。 - supercat

21

之所以这样做,是因为在IL代码中,指向test1的引用被分配给局部变量CS$2$0000。在C#中你将test1设置为null,但是lock结构以一种维护单独引用的方式进行编译。

C#编译器实际上很聪明地做到了这一点。否则,就有可能规避lock语句应该执行的释放锁定的保证, 退出临界区时释放锁定。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接