如果在锁定对象内部发生异常,该对象是否仍然保持锁定状态?

99

在一个C#线程应用中,如果我锁定了一个对象,比如一个队列,如果发生异常,该对象会保持锁定状态吗?这是伪代码:

int ii;
lock(MyQueue)
{
   MyClass LclClass = (MyClass)MyQueue.Dequeue();
   try
   {
      ii = int.parse(LclClass.SomeString);
   }
   catch
   {
     MessageBox.Show("Error parsing string");
   }
}

据我理解,在 catch 后面的代码不会执行,但我一直在想这个锁是否会被释放。


1
作为最后的思考(请参见更新)-您应该只在出队列期间持有锁定状态...在锁定状态之外进行处理。 - Marc Gravell
1
由于异常已被处理,因此catch后面的代码会执行。 - cjk
谢谢,我可能错过了那个问题,我应该删除这个问题吗? - Vort3x
4
这个问题似乎不适合使用示例代码来解答,但是问题本身是非常合理的。 - SalvadorGomez
由C#设计师 - 锁定和异常 - Jivan
6个回答

108

我注意到在这个旧问题的回答中,没有人提到在异常情况下释放锁是一件极其危险的事情。 是的,在C#中的锁语句具有“finally”语义; 当控制正常或异常退出锁时,锁将被释放。 你们都在谈论这件事好像它是一件好事,但这是一件坏事! 如果您有一个被锁定的区域引发了未处理的异常,那么正确的做法是立即终止已经发生故障的进程,以防止破坏更多用户数据,而不是释放锁并继续运行

从这样的角度来看:假设您有一个有门锁和一排人等待外面的浴室。浴室里的炸弹爆炸了,杀死了里面的人。您的问题是“在这种情况下,锁是否会自动解锁,以便下一个人可以进入浴室?” 是的,会的。那不是一件好事。刚才那里有炸弹爆炸并且杀死了人!管道可能已经损坏,房子不再结构健全,而且可能还有另一枚炸弹在里面。正确的做法是尽快让所有人离开并拆除整个房子。

我的意思是,仔细想一下:如果您为了从数据结构中读取而锁定了代码区域,而该数据结构中的某些内容引发异常,很有可能是因为数据结构已经损坏了。用户数据现在出现了问题;此时您不想尝试保存用户数据,因为那样会保存已经损坏的数据。只需终止进程即可。

如果你在代码中锁定了一个区域,以便执行变异而没有其他线程在同一时间读取状态,但该变异引发异常,则如果数据在此之前不是损坏的,现在肯定会受到影响。这正是锁定所要保护的情况。此时等待读取该状态的代码将立即获得对已损坏状态的访问,并可能导致自身崩溃。此时应该终止该进程。
无论如何,在锁定内部引发异常都是一个坏消息。正确的问题不是“如果出现异常,我的锁是否会被清除?”而是“如何确保锁定内部永远不会出现异常?如果出现异常,我该如何构建程序使变异回滚到之前的良好状态?”

32
在我看来,这个问题与锁定相当无关。如果你遇到预期的异常,你需要清理所有东西,包括锁定。如果你遇到意外的异常,无论是否有锁定,都会出现问题。 - CodesInChaos
13
我认为上述情况是一种概括性描述。有时候,例外情况描述的是灾难性事件,有时则不是。每个人在代码中使用它们的方式都不同。假设异常=catastrophic(灾难性),进程终止的情况太过具体了,出现例外情况也可能只是表示一个非常规但并非灾难性的事件,这是完全合理的。事实上,即使可能是灾难性事件,这也不会影响问题的有效性。沿着同样的思路,你也许会决定永远不处理任何异常,在这种情况下,程序将退出... - Gerasimos R
1
想到一个问题,如果 catch 块可以紧跟 lock 块,那么不是使用 try-catch,而是使用 lock-catch(当然,在内部它仍然只是带有 Monitor 的 try):lock(obj) {} catch(Exception ex){...} 然后 catch 仍然会在 finally(锁将丢失的地方)之前执行。至少它能明确一些问题,用户就能够处理锁即将丢失的情况。这只是我在胡思乱想,不太确定。 - Nicholas Petersen
2
@NicholasPetersen:首先,是的,我确实害怕和厌恶锁!编程是将小问题的正确解决方案组合成大问题的正确解决方案的行为,但包含锁的代码是不可组合的。锁定 actively work against 使其可用的语言特性!现在,话虽如此,如果我们要在同一语言中使用锁和异常,并且如果锁语句是 try-finally 的糖,那么是的,我非常喜欢您的想法,即制作 catch 块。好主意! - Eric Lippert
3
如果“不可组合”这个概念不太清楚的话,请看这里:假设我们有一个名为“transfer”的方法,它接受两个列表s和d,锁定s,锁定d,从s中移除一项,将该项添加到d中,解锁d,解锁s。只有当没有人在同一时间尝试从列表X向列表Y转移时,该方法才是正确的。转移方法的正确性不能让您从中构建出解决更大问题的正确方案,因为锁是全局状态的不安全变异体。要安全地“转移”,您必须了解程序中的每个锁 - Eric Lippert
显示剩余7条评论

94

首先,你考虑过使用 TryParse 吗?

in li;
if(int.TryParse(LclClass.SomeString, out li)) {
    // li is now assigned
} else {
    // input string is dodgy
}

锁会因两个原因而被释放;首先, lock 本质上是:

Monitor.Enter(lockObj);
try {
  // ...
} finally {
    Monitor.Exit(lockObj);
}

其次,你捕获并未重新抛出内部异常,因此lock实际上从未看到异常。当然,在弹出消息框的持续时间内,您将一直保持锁定状态,这可能是一个问题。

因此,除了最致命的灾难性不可恢复异常外,它将在所有情况下被释放。


17
我知道"TryParse",但它与我的问题无关。这只是一段简单的代码用来解释问题 - 并不是真正涉及要解析的内容。请将解析部分替换为任何代码,以强制触发catch并让你感到满意。 - Khadaji
16
如何使用 throw new Exception("for illustrative purposes"); 进行演示呢?;-p - Marc Gravell
2
除非在 Monitor.Entertry 之间发生 TheadAbortException,否则不会发生异常:http://blogs.msdn.com/ericlippert/archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx - Igal Tabachnik
2
致命的灾难性不可恢复的异常,就像交叉流一样。 - Phil Cooper

44

是的,那样会被正确地释放;lock 的作用类似于 try/finally 语句,其中 Monitor.Exit(myLock) 在 finally 中执行,因此无论如何退出都会被释放。顺便提一下,最好避免使用 catch(... e) {throw e;},因为��会损坏 e 上的堆栈跟踪;最好完全不捕获它,或者选择使用 throw; 而不是 throw e; 进行重新抛出。

如果你真的想知道,C#4 / .NET 4 中的锁是:

{
    bool haveLock = false;
    try {
       Monitor.Enter(myLock, ref haveLock);
    } finally {
       if(haveLock) Monitor.Exit(myLock);
    }
} 

15

"锁语句被编译成对 Monitor.Enter 的调用,随后是 try…finally 代码块,在 finally 代码块中会调用 Monitor.Exit。

无论是 x86 还是 x64 平台的 JIT 代码生成,都能确保在 Monitor.Enter 调用与紧随其后的 try 块之间不会发生线程中止。

摘自: 此网站


1
至少有一种情况是不正确的:在 .net 4 版本之前的调试模式下,线程中止。原因是 C# 编译器在 Monitor.Entertry 之间插入了一个 NOP,因此 JIT 的“紧随其后”条件被违反了。 - CodesInChaos

6

您的锁将被正确释放。一个 的作用如下:

try {
    Monitor.Enter(myLock);
    // ...
} finally {
    Monitor.Exit(myLock);
}

finally块保证会执行,无论你如何离开try块。


实际上,“没有”代码是“保证”执行的(例如,您可以拔掉电源线),而在4.0中锁定看起来并不完全像这样-请参见此处。 - Marc Gravell
1
@MarcGravell:我考虑过关于这两个确切的点加上两个脚注。然后我想这不会很重要 :) - Ry-
1
@MarcGravel: 我认为大家都默认情况下并不是在谈论“拔插头”这种情况,因为这不是程序员所能控制的事情 :) - Vort3x

6

仅仅是为了补充Marc优秀回答的内容。

类似这样的情况正是存在lock关键字的原因。它帮助开发人员确保锁在finally块中被释放。

如果你被迫使用Monitor.Enter/Exit例如为了支持一个超时,你必须确保将Monitor.Exit调用放在finally块中以确保在异常情况下正确释放锁。


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