多线程异常并锁定

时间:2017-11-20 09:21:13

标签: java c++ multithreading error-handling exception-handling

当锁内部抛出异常时,我们应该释放锁吗?

当然,如果预期会出现此异常并且我们可以从中恢复,我们应该执行适当的操作以使锁定受保护的数据保持一致并释放锁定。如果异常是意外的,例如,。,OutOfMemoryExceptions,LinkErrorExceptions,NullPointerExceptions,它们可能在任何地方被抛出,我们想要终止程序以阻止进一步的数据损坏。在这种情况下,我们应该在终止程序之前释放锁吗?

我不想释放锁。这里有一个很好的帖子:

Does a locked object stay locked if an exception occurs inside it?

  

我注意到在他们对这个老问题的回答中没有人提到释放锁定异常是一件非常危险的事情。是的,C#中的锁定语句最终"最终"语义;当控制器正常或​​异常退出锁定时,锁定被释放。你们都在谈论这件事,好像这是一件好事,但这是一件坏事!如果你有一个引发未处理异常的锁定区域,那么正确的做法就是在它破坏更多用户数据之前立即终止病态进程,而不是释放锁定并继续运行。

     

这样看:假设你的卫生间门上有一把锁,还有一排人在外面等着。浴室里的炸弹爆炸了,杀死了那里的人。你的问题是"在这种情况下,锁会自动解锁,以便下一个人可以进入浴室吗?"是的,它会的。那不是一件好事。一枚炸弹刚刚在那里爆炸并杀死了一个人!管道可能被破坏,房子不再结构健全,那里可能还有另一枚炸弹。正确的做法是尽快让所有人离开并拆除整个房子。

     

我的意思是,想一想:如果你锁定了一个代码区域,以便从数据结构中读取而不在另一个线程上进行变异,并且该数据结构中的某些东西引发了异常,那么它很可能是因为数据结构已损坏。用户数据现在搞砸了;此时您不想尝试保存用户数据,因为您正在保存损坏的数据。只需终止该过程。

     

如果您锁定了一个代码区域,以便在没有其他线程同时读取状态的情况下执行突变,并且突变抛出,那么如果数据之前没有损坏,那么现在肯定是。这正是锁应该防范的情况。现在,等待读取该状态的代码将立即被授予访问腐败状态的权限,并且可能本身崩溃。同样,正确的做法是终止流程。

     

无论你如何切片,锁内的异常都是坏消息。要问的正确问题不是"如果发生异常,我的锁会被清理吗?"要问的正确问题是"我如何确保锁内没有异常?如果有,那么我如何构建我的程序,以便突变回到以前的良好状态?"

在这种情况下,C ++ RRID模式不会释放锁:

...
{
   // the destructor will release the lock when
   // we go out of this block
   AutoReleseLock lock(&mLock);

   ...
   // Exception thrown here.
   foo();
   ...
}
...

如果在某处捕获了抛出的异常,则会释放锁定。否则,将直接调用std :: terminate()而不释放锁。

但在Java中:

synchronized (monitor) {
    ...
    // Exception thrown here.
    foo();
    ...
}

 lock();
 try {
    ...
    // Exception thrown here.
    foo();
    ...
 } finally {
     unlock();
 }

在我们到达未处理的异常处理程序之前,无论是否捕获异常,都会释放锁定。其他线程可能会读取损坏的不一致数据并执行不可预测的操作,例如,删除错误的文件。

那么,如何在不释放对意外异常的锁定的情况下获得与C ++类似的行为来终止程序而不需要很多丑陋的代码呢?

更新

异常安全或自杀以防止数据损坏都是好主意。但允许不可预测的行为并不是一个好主意,即使它很少见。

但是,异常安全,因为持有锁可能需要很多繁琐的逻辑。例如,Java中的OutOfMemoryException。

synchronized {
   nameSet.add(newPeople.name);
   ageSet.add(newPeople.age);
   jobSet.add(newPeople.job);
}

将成为:

synchronized {
   nameSet.add(newPeople.name);

   try {
      ageSet.add(newPeople.age);
   } catch (OutOfMemoryException e) {
      nameSet.remove(newPeople.name);
      throw e;
   }

   try {
      jobSet.add(newPeople.job);
   } catch (OutOfMemoryException e) {
      nameSet.remove(newPeople.name);
      ageSet.remove(newPeople.age);
      throw e;
   }
}

而且,仍然存在错误。在我们将名称/年龄/工作添加到集合之前,我们应该确定它是否已经存在。

大多数系统都希望OutOfMemoryException是一个程序终止操作,而不是一般的例外,或者我们几乎每个地方都会捕获它们。在这种情况下,此kill操作不像C ++中那样干净,并且会释放锁以留下其他线程要读取的不一致数据。其他线程可能会将此数据保留在磁盘上。

2 个答案:

答案 0 :(得分:1)

如果“锁定内部”发生异常,则无法解释锁是否应该被释放。但是,通常情况下释放锁是有意义的。

这取决于应用程序的要求。需求可能包括事务回滚语义(如果在事务期间抛出异常,程序状态和关联数据存储被回滚到没有证据表明甚至尝试过操作的点)或其他异常安全保证(例如,如果异常是抛出,所有对象都处于某种状态,因此可以安全地销毁它们。在这种情况下,无论是在堆栈展开过程中还是在异常处理程序中释放锁都非常有意义。

相反,应用程序要求可能是 - 如果抛出异常 - 禁用所有受影响的功能。在这种情况下,锁定可能是有意义的 - 至少在发生错误恢复之前。实际上,这种情况很少见。而且,如果应用程序本身能够进行错误恢复,那么应用程序最终会释放锁定是有意义的。如果没有,那么这意味着需要其他系统组件来解决错误条件而不进行纠正。

我同意Kayaman的评论,即“锁在浴室”的例子是荒谬的。它假设当炸弹爆炸后门被解锁时,外面的人唯一允许的动作就像旅鼠一样进入里面。这在现实生活中并非如此,在系统设计中并非如此。

一般来说,如果我在错误情况下要求锁保持锁定,我会将其视为需求开发不完整的提示。因此,我会寻求制定完全防止错误条件或适当响应它的要求(纠正措施,启动系统刷新等)。

答案 1 :(得分:1)

  

[在这些Java示例中]无论是否捕获到异常,锁都将被释放,......并且其他线程可能会读取损坏的不一致数据并执行不可预测的操作。

作者有责任确保代码在所有情况下都按预期执行。如果程序打算在抛出异常后继续运行,那么程序员的工作就是实现这一目标。

在两个Java示例中,锁都被释放,但是当抛出异常时,显示的代码不会执行任何其他清理。但是,互斥体的重点是防止其他线程在临时,不一致的状态下看到共享数据。因此,如果在共享数据 处于这种状态时抛出异常是可能的,那么程序员必须编写一个处理程序,在可能重新抛出之前再次将事情放回去异常或通过其他方式离开互斥锁。