【问题标题】:ReaderWriterLockSection: A bad idea?ReaderWriterLock 部分:一个坏主意?
【发布时间】:2010-12-26 09:26:26
【问题描述】:

在编写一些线程代码时,我一直在使用ReaderWriterLockSlim 类来处理对变量的同步访问。这样做时,我注意到我一直在编写 try-finally 块,每个方法和属性都一样。

看到有机会避免重复自己并封装这种行为,我构建了一个类ReaderWriterLockSection,旨在用作锁的瘦包装器,可与 C# using 块语法一起使用。

类大多如下:

public enum ReaderWriterLockType
{
   Read,
   UpgradeableRead,
   Write
}

public class ReaderWriterLockSection : IDisposeable
{
   public ReaderWriterLockSection(
       ReaderWriterLockSlim lock, 
       ReaderWriterLockType lockType)
   {
       // Enter lock.
   }

   public void UpgradeToWriteLock()
   {
       // Check lock can be upgraded.
       // Enter write lock.
   }

   public void Dispose()
   {
       // Exit lock.
   }
}

我使用的部分如下:

private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

public void Foo()
{
    using(new ReaderWriterLockSection(_lock, ReaderWriterLockType.Read)
    {
        // Do some reads.
    }
}

对我来说,这似乎是一个好主意,它使我的代码更易于阅读并且看起来更健壮,因为我永远不会忘记释放锁。

任何人都可以看到这种方法的问题吗?有什么理由这是一个坏主意吗?

【问题讨论】:

  • 我建议你改用结构体,以避免 GC 开销。
  • 使用结构体会导致装箱,因为结构体将实现 IDisposable,这意味着对构造函数和 Dispose 方法的所有调用都会对结构体进行装箱/拆箱,这会减慢主线程的速度,还会导致一些箱体停留在 GC 堆上,从而否定结构的使用。
  • 具有实现 IDisposable 的结构/值类型的 using 子句不会将值装箱,而是直接在结构上调用 Dispose,这将按预期工作。如果他可能会传递节值的可能性很小,那么结构是不可用的,但如果唯一的用途是用于这样的 using 块,那么结构将避免 GC 开销并按建议工作。
  • 当然,如果您明确地将其强制转换为 IDisposable,或者将其从 SectionFactory-type-method 作为 IDisposable 返回,那么它将被装箱。我建议您直接返回和/或将其声明为值类型,这将避免装箱和别名。换句话说,如果 ReaderWriterLockSection 是一个结构,那么问题底部的代码就可以工作。
  • 详情请见billrob.com/archive/2006/05/12/530.aspx(Eric Lippert 的评论)

标签: c# .net multithreading readerwriterlockslim


【解决方案1】:

在包装锁以促进“使用”模式时,我建议的一件事是在锁中包含“危险状态”字段;在允许任何密码进入锁之前,密码应该检查危险状态。如果设置了危险状态,并且试图进入锁的代码没有传递一个特殊的参数来说明它期望它可能是,那么获取锁的尝试应该抛出一个异常。要将受保护资源暂时置于不良状态的代码应设置危险状态标志,做需要做的事情,然后在操作完成且对象处于安全状态后重置危险状态标志。

如果在设置危险状态标志时发生异常,则应释放锁,但应保持设置危险状态标志。这将确保想要访问资源的代码会发现资源已损坏,而不是永远等待锁被释放(如果没有“using”或“try-finally”块,这将是结果)。

如果被包装的锁是 ReaderWriterLock,那么获取“写入器”锁自动设置危险状态可能会很方便;不幸的是,using 块使用的 IDisposable 无法确定该块是干净退出还是通过异常退出。因此,我不知道有什么方法可以在语法上使用诸如“使用”块之类的东西来保护“危险状态”标志。

【讨论】:

    【解决方案2】:

    这里还有一个考虑因素,你可能正在解决一个你不应该解决的问题。我看不到您的其余代码,但我可以从您看到这种模式的价值中猜到。

    只有在读取或写入共享资源的代码引发异常时,您的方法才能解决问题。隐含的是,您不处理执行读/写的方法中的异常。如果你这样做了,你可以简单地释放异常处理代码中的锁。如果您根本不处理异常,线程将死于未处理的异常,您的程序将终止。那么释放锁就没有意义了。

    所以在调用堆栈的较低位置有一个 catch 子句,它捕获异常并处理它。此代码必须恢复程序的状态,以便它可以有意义地继续,而不会生成坏数据或因状态改变引起的异常而死。该代码有一项艰巨的工作要做。它需要在没有任何上下文的情况下以某种方式猜测读取或写入了多少数据。弄错了,或者只是部分正确,可能会对整个程序造成很大的不稳定。毕竟,它是一个共享资源,其他线程正在从中读取/写入。

    如果您知道如何执行此操作,那么请务必使用此模式。你最好测试一下。如果您不确定,请避免将系统资源浪费在无法可靠解决的问题上。

    【讨论】:

    • 虽然我不需要逻辑进程的 try-finally 块,但它确实让我知道如果线程被中止,ThreadAbortException 将释放所有锁。此外,图案方便、清晰、不显眼。
    • 您不能安全地中止正在使用共享资源的线程,除非您中止 所有 使用该资源的线程。 RWLS 的状态现在不再重要。
    • 即使线程正在中止自己,即它发生在可预测的时刻,而不是在另一个操作的中间,也是如此?
    • 没有。但是保护 RWLS 是没有意义的,因为它永远不会为该线程激活锁。
    • 如果持有 writer-lock 的代码发生了不好的事情,我可以理解,假设受保护的资源可能处于错误状态。但是读取器锁的持有者怎么会破坏共享资源的状态呢?
    【解决方案3】:

    我通常沉迷于这种代码含糖的甜点!

    这是一个更易于用户阅读的变体,位于您的 API 之上

    public static class ReaderWriterLockExt{
      public static IDisposable ForRead(ReaderWriterLockSlim rwLock){
        return new ReaderWriterLockSection(rwLock,ReaderWriterLockType.Read);
      }
      public static IDisposable ForWrite(ReaderWriterLockSlim rwLock){
        return new ReaderWriterLockSection(rwLock,ReaderWriterLockType.Write);
      }
      public static IDisposable ForUpgradeableRead(ReaderWriterLockSlim wrLock){
        return new ReaderWriterLockSection(rwLock,ReaderWriterLockType.UpgradeableRead);
      }
    }
    
    public static class Foo(){
      private static readonly ReaderWriterLockSlim l=new ReaderWriterLockSlim(); // our lock
      public static void Demo(){
    
    
        using(l.ForUpgradeableRead()){ // we might need to write..
    
          if(CacheExpires()){   // checks the scenario where we need to write
    
             using(l.ForWrite()){ // will request the write permission
               RefreshCache();
             } // relinquish the upgraded write
          }
    
          // back into read mode
          return CachedValue();
        } // release the read 
      }
    }
    

    我还建议使用一个变体,它接受一个在 10 秒内无法获得锁时调用的 Action 委托,我将把它作为练习留给读者。

    您可能还想在静态扩展方法中检查空 RWL,并确保在释放它时锁存在。

    干杯, 弗洛里安

    【讨论】:

    • 我知道这只是一个演示,但这根本不是线程安全的。您的RWLSlim 是本地的,这意味着每次调用都将使用不同的锁实例。
    • 不错,我会把它移出本地人,我最初并没有打算写一个长方法,抱歉造成混乱
    【解决方案4】:

    嗯,我觉得没问题。 Eric Lippert 之前曾写过关于在“非资源”场景中使用 Dispose 的危险,但我认为这将被视为一种资源。

    在升级场景中这可能会让生活变得棘手,但你总是可以在那个时候回退到更手动的代码。

    另一种选择是编写单个锁获取/使用/释放方法,并提供在作为委托持有锁时要执行的操作。

    【讨论】:

    • 您是否愿意详细说明升级方案以及为什么使用此类可能比替代方法更棘手?我喜欢使用带有委托的方法的声音,尤其是当它将我的代码的“动作”部分与“线程安全”部分分开时。
    • @Jon Skeet:您能否提供 Eric Lippert 帖子的链接?我找不到它。谢谢。
    • @Kamarey:这是 Stack Overflow 上的一个答案,但恐怕需要一段时间才能找到它。 @Programming Hero:关于升级锁,我认为这实际上只是一个小心的情况,当你不想释放锁时,你不要释放它,而是确实释放它您期望的时间。我只是在提醒您注意一个区域,您可能需要特别小心:)
    • Kamarey,以下是 Eric 对该主题的看法:stackoverflow.com/questions/1095438/…
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-12
    • 2012-04-02
    • 1970-01-01
    • 1970-01-01
    • 2020-12-12
    相关资源
    最近更新 更多