为什么锁定可以确保底层监视器被释放,而直接使用监视器则不行?

3

MSDN文章Thread Synchronization (C# Programming Guide)指定:

lock (x)
{
    DoSomething();
}

等价于:

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
} 

然后是这个:

“通常使用lock关键字优于直接使用Monitor类,因为lock确保即使受保护的代码引发异常,底层监视器也会被释放。”

这句话的意思是,最后一个代码片段使用监视器时,“即使受保护的代码引发异常,底层监视器也不会被释放”吗?
为什么呢?

好吧,我对这两个相互矛盾的断言“等效”和“不等效”感到困惑(一个用法可保证,另一个等效却不能)。


太多优秀的答案了。很难决定哪个是正确的。感谢大家! - Fulproof
5个回答

5
粗体字所指的案例更像是这样:
Monitor.Enter(obj);
DoSomethingThatThrows();
Monitor.Exit(obj);

如果没有try-finally语句,抛出异常将绕过Monitor.Exit的调用。


3
如果锁和提供的代码位是功能上等价的,两者显然都确保它将被释放,因为有一个 finally 子句。但是,如果你只使用监视器而没有 finally,可能会遇到麻烦,导致死锁。至少,我认为文章的解释是这个意思。

我也是这样理解的。 - NDJ

3
在x64体系结构中(直到VS2008 JIT,根据J.Duffy的说法,在某些情况下,当编译为任何CPU时,没有/o+开关),可能会在Monitor.EnterTry语句之间放置IL指令。如果异常发生时堆栈指针位于此指令处,则lock将永远不会被释放。
但是,lock关键字的代码生成防止了这种情况发生。
这可能是为什么他们建议使用lock关键字的原因。
参考:Monitor.Enter, Thread aborts

3
如果你查看由4.0编译器为anycpu生成的IL并将其反转为C#,最接近的lock等效实现我能够得到的样子是:
object x = new object();
bool lockTaken = false;
// lock
try{
    System.Threading.Monitor.Enter(x, ref lockTaken)
    DoSomeThing();
}
finally
{
   if (lockTaken)
   {
        System.Threading.Monitor.Exit(x);
   }
}

所有的工作都是为了防止出现锁定被占用,线程中止但锁未被释放导致竞争/死锁的情况。警告基本上告诉你在好的和首要的失败情况下平衡Enter和Exit调用。lock语句是实现该目标的最简单的抽象。

基于这个IL:

    IL_0000: nop
    IL_0001: ldc.i4.0
    IL_0002: stloc.0
    .try
    {
        IL_0003: ldsfld object p::x
        IL_0008: dup
        IL_0009: stloc.1
        IL_000a: ldloca.s 0
        IL_000c: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&)
        IL_0011: nop
        IL_0012: nop
        IL_0013: call void p::DoSomething()
        IL_0018: nop
        IL_0019: nop
        IL_001a: leave.s IL_002c
    } // end .try
    finally
    {
        IL_001c: ldloc.0
        IL_001d: ldc.i4.0
        IL_001e: ceq
        IL_0020: stloc.2
        IL_0021: ldloc.2
        IL_0022: brtrue.s IL_002b

        IL_0024: ldloc.1
        IL_0025: call void [mscorlib]System.Threading.Monitor::Exit(object)
        IL_002a: nop

        IL_002b: endfinally
    } // end handler

    IL_002c: nop
    IL_002d: ldsfld object p::x
    IL_0032: call void [mscorlib]System.Threading.Monitor::Enter(object)
    IL_0037: nop
    .try
    {
        IL_0038: nop
        IL_0039: call void p::DoSomething()
        IL_003e: nop
        IL_003f: nop
        IL_0040: leave.s IL_0050
    } // end .try
    finally
    {
        IL_0042: nop
        IL_0043: ldsfld object p::x
        IL_0048: call void [mscorlib]System.Threading.Monitor::Exit(object)
        IL_004d: nop
        IL_004e: nop
        IL_004f: endfinally
    } // end handler

    IL_0050: nop
    IL_0051: ret

谢谢你的无意义IL :) - Fulproof

2
这意味着当您使用 Monitor 时,可能会忘记使用 try-finally。实际上,你会发现很多人只在块的开头使用 Monitor.Enter,并在块的结尾使用 Monitor.Exit
这并不能保证 Monitor.Exit 一定会发生,因为异常可能导致代码在块的中间停止运行。

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