【问题标题】:locking a resource via lock within try. Is it wrong?通过 try 中的 lock 锁定资源。这是错的吗?
【发布时间】:2011-05-14 06:34:51
【问题描述】:

在 try 块中使用 lock 有什么问题吗?我记得在某处读到,我们应该始终尝试将最少数量的代码放在 try 块中,并在内部锁定自身使用 try-finally 块,你们看到这里有什么问题吗?我需要处理一个事实,即该锁中的代码块可以抛出异常

try  
{  
   lock(syncblk)  
   {  
        // do some processing  
    }  

}  
catch(Exception e)  
{  
    // do something with exception  
}  

【问题讨论】:

  • 你所做的是正确的。 lock 语句确保在块结束时释放锁,即使抛出异常也是如此。
  • @Jim:你说这是件好事。 这是一件坏事。如果抛出了意外的异常,那么释放锁可以防止死锁,当然,但它也解锁了对现在已损坏以致导致异常的状态的访问!
  • @Jim:如果您正在考虑的只是代码完成时锁是否解锁出于任何原因,那么他所做的是正确的。对于某人来说,这本身可能是一个目标,但这并不是编写好软件的好方法。
  • @Eric:点了。看来我救了树,让森林烧了。
  • @Eric:使用锁的三个原因:(1)防止其他线程在不满足其不变量时看到该对象,(2)防止其他线程将对象进入与锁定线程期望的状态不一致的状态,或 (3) 避免以由于原因 #2 获取它的线程可能不期望的方式更改项目。如果由于原因#2 或#3 获取锁的代码抛出异常,则释放锁没有任何问题。我会进一步建议,如果#1是一个问题,适当的补救措施是......

标签: c# exception-handling synchronization


【解决方案1】:

我需要处理该锁块中的代码可能引发异常的事实

还有你的问题。这是一个可怕的情况。

你为什么首先锁定?通常你锁定某些东西的原因是因为你想实现以下逻辑:

  • 锁门
  • 弄得一团糟
  • 清理它
  • 解锁门

如果你这样做,那么任何尊重锁着的门的人都不会看到这个烂摊子。

例如,您可能希望以线程安全的方式交换变量“left”和“right”的值,因此您:

  • 拿锁
  • 将left变量读入tempLeft
  • 将正确的变量读入 tempRight
  • 将 tempLeft 写入右
  • 我们只是弄得一团糟; 'right' 的原始值丢失了
  • 将 tempRight 写入 left
  • 我们已经收拾残局了,世界又恢复了正常
  • 释放锁

现在假设在搞砸之后抛出异常。怎么了? 我们直接跳到解锁,把混乱留给另一个线程看

这就是为什么你应该永远不要在锁内抛出异常;它完全违背了锁的目的!锁的全部意义在于确保所有线程始终观察到状态是一致的,但负责清理混乱的线程除外。

如果您有一个可以从锁内部抛出的异常,那么最好的办法就是摆脱这种可怕的情况。如果您不能这样做,那么请确保您可以(1)在异常逃脱锁定后立即彻底销毁该进程,以便您造成的混乱不会导致数据丢失或其他伤害 - 执行@987654321 @ 并将进程从轨道中删除,这是唯一可以确定的方法——或者 (2) 编写回滚代码来撤消您在退出锁之前尝试的任何操作;也就是收拾烂摊子回到原来的状态。

如果后者是你的策略,那么不要将 try 块放在 锁之外;它在那里没用,因为即时控制通过异常离开锁,另一个线程可能会因为你暴露给它的混乱而崩溃和死亡。将处理异常的try 放入锁中:

lock(whatever)
{
    try
    {
        MakeAMess();
    }
    finally
    {
        CleanItUp();
        // Either by completing the operation or rolling it back 
        // to the pre-mess state
    }
}

如果您有很强的可靠性要求,那么处理可能引发异常的锁定临界区是一项极其困难的编程任务,最好留给专家来完成;如果您经常遇到这种情况,您可以考虑使用 constrained execution region

【讨论】:

  • 获取锁的原因有很多很多。假设在锁中抛出的任何异常都会使事情处于混乱状态,这是过于悲观的。实际上,我猜想绝大多数锁是由从未写入受保护数据结构的代码获取的,而只是想避免让其他代码这样做,并且大多数锁是由确实写入数据的代码获取的结构旨在防止数据结构与本地缓存信息或关于它的“计划”之间的不一致,而不是数据结构本身的各个部分之间的不一致。
  • 考虑一个 AppendableList,例如,它像一个列表一样工作,但唯一支持的操作是 add-at-end。当需要增加列表时,会创建一个新数组,将旧项复制到其中,列表的数组引用指向新数组,将新项写入那里,并增加项计数。 AppendableList 本身在添加操作期间始终处于一致状态,但无论如何 Add 不是线程安全的。如果“添加”操作被挂起并且允许第二个线程添加到列表中,那么数据结构就可以了......
  • ...直到第一个线程尝试完成其操作。如果第一个线程要部分完成它的操作,但可以保证永远不会回来完成它,那么使数据结构对其他线程可用是没有问题的。虽然我同意你的观点,让数据处于无效状态的代码应该在释放锁之前清理它,或者有一些方法表明数据处于无效状态,我认为假设在一个锁会破坏状态太悲观了。
  • @supercat:假设在锁中抛出的任何异常都会使事情处于混乱状态,这并不过分悲观。您使用锁这一事实意味着您希望锁能够保护某物。如果失去这种保护不会破坏任何东西,那你为什么还要使用锁呢?
  • @supercat:我当然同意你的观点,可以概括为“抛出读锁不会破坏任何东西”。当然。但是,让我们在这里看看更大的图景。然后假定的场景是读取受关键部分保护的内容导致异常。这听起来像一个很好的情况吗? 单独阅读导致异常的关键部分到底做了什么???这听起来像是一个需要修复的错误,而不是主线场景。
【解决方案2】:

我认为您可以按照自己的方式进行操作,但这里是 MSDN 对 lock 的描述,供您参考。更多信息请参考http://msdn.microsoft.com/en-us/library/ms173179.aspx

使用锁 (C#) 或 SyncLock (Visual Basic) 关键字一般是 优于使用 Monitor 类 直接,因为锁定或 SyncLock 更简洁,因为 lock 或 SyncLock 确保 底层监视器被释放,甚至 如果受保护的代码抛出 例外。这是通过 finally 关键字,执行 其关联的代码块不管 是否抛出异常。

所以我不确定你指的是哪种异常,但如果你担心你可能因为异常而无法释放锁,你不必担心。

【讨论】:

  • -1:他不必担心锁被锁定,但他绝对应该担心让他首先引入锁的状态地点。
【解决方案3】:

你总是可以像这样使用更长的语法:

System.Threading.Monitor.Enter(x);
try {
   ...
}
catch(Exception e)
{
}
finally {
   System.Threading.Monitor.Exit(x);
}

【讨论】:

  • -1:他为什么要这么做?它有什么区别?它能解决任何问题吗?
猜你喜欢
  • 1970-01-01
  • 2021-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-10
  • 1970-01-01
  • 1970-01-01
  • 2013-04-25
相关资源
最近更新 更多