【问题标题】:Lock statement vs Monitor.Enter methodLock 语句与 Monitor.Enter 方法
【发布时间】:2011-02-19 16:37:45
【问题描述】:

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

我们有一个类——我们称之为Test——带有一个Finalize方法。在 Main 方法中有两个代码块,我在其中使用了一个 lock 语句和一个 Monitor.Enter() 调用。另外,我在这里有两个 Test 类的实例。 实验非常简单:将锁定块中的 Test 变量清空,然后尝试使用 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();
    }
}

这个例子的输出是:

手动收集1。手动收集2。 手动收集 3. 结束类 名称 Test2。手动收集 4. 最后一个 finally 块中的空引用异常,因为 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回收的,而是在使用Monitor.Enter 是否收集完毕?

【问题讨论】:

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


    【解决方案1】:

    我看不出 lock 语句和 Monitor.Enter 调用有什么区别。

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

    注意 C# 3.0 规范对此主题的规定:

    “lock (x) ...”形式的 lock 语句,其中 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 中,Enter 是 inside,而不是在 C# 3 中的外部。)

    【讨论】:

    • 为什么在 4.0 中决定将它移到 try 中?
    • 是的,现在很清楚,我自己没有看到差异是我的错。感谢您的解释。
    • 我想知道 .net 是否会为创建 IDisposable 对象提供等价物(例如,让正在构建的对象将自身存储在 byref 参数中,这样如果构造函数抛出异常,部分构造的对象可以被 Disposed(当然,Dispose 必须意识到对象可能没有完全构造,但在许多情况下不应该太难)。
    【解决方案2】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-02-08
      • 2011-02-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-23
      相关资源
      最近更新 更多