C# ReaderWriterLockSection:一个坏主意?

C# ReaderWriterLockSection:一个坏主意?,c#,.net,multithreading,readerwriterlockslim,C#,.net,Multithreading,Readerwriterlockslim,在编写一些线程化代码时,我使用了ReaderWriterLockSlim类来处理对变量的同步访问。这样做时,我注意到我总是在编写try-finally块,每个方法和属性都是一样的 看到一个避免重复并封装这种行为的机会,我构建了一个类,ReaderWriterLockSection,打算用作锁的薄包装,可以使用块语法与C#一起使用 课程内容主要如下: public enum ReaderWriterLockType { Read, UpgradeableRead, Write }

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

看到一个避免重复并封装这种行为的机会,我构建了一个类,
ReaderWriterLockSection
,打算用作锁的薄包装,可以使用块语法与C#
一起使用

课程内容主要如下:

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.
    }
}
我使用该部分如下:

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.
    }
}
对我来说,这似乎是一个好主意,它使我的代码更容易阅读,而且看起来更健壮,因为我永远不会忘记释放锁


有人能看出这种方法有什么问题吗?有什么理由认为这是个坏主意吗?

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

在升级场景中,这可能会让生活变得棘手,但在这一点上,您总是可以退回到更手动的代码位


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

我通常沉迷于这种代码甜点

在您的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秒钟内无法获得锁时调用该委托,我将把它作为练习留给读者

您可能还希望在静态扩展方法中检查null RWL,并确保在释放锁时该锁存在

干杯,
Florian

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

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

因此,在调用堆栈的较低位置有一个catch子句,用于捕获异常并处理它。此代码必须恢复程序的状态,以便它可以继续运行,而不会生成错误数据,或者由于状态更改导致的异常而死亡。那个代码很难完成。它需要以某种方式猜测在没有任何上下文的情况下读取或写入了多少数据。弄错了,或者只是部分对了,可能会严重破坏整个计划的稳定。毕竟,它是一个共享资源,其他线程正在从中读/写


如果您知道如何做到这一点,那么请务必使用此模式。不过你最好测试一下。如果您不确定,请避免将系统资源浪费在无法可靠修复的问题上。

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

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


如果被包装的锁是ReaderWriterLock,则可以方便地让“writer”锁的获取自动设置危险状态;不幸的是,
using
块使用的IDisposable无法确定该块是干净退出还是通过异常退出。因此,我不知道如何在语法上使用诸如“using”块之类的东西来保护“danger state”标志。

您是否愿意详细说明升级场景,以及为什么使用此类可能比其他方法更棘手?我喜欢在委托中使用方法的声音,特别是因为它将我代码中的“操作”部分与“线程安全”部分区分开来。@Jon Skeet:你能提供到Eric Lippert文章的链接吗?我找不到它。谢谢。@Kamarey:这是关于堆栈溢出的回答,但恐怕需要一段时间才能找到@编程英雄:关于升级锁,我认为这实际上只是一个小心的例子,不要在你不想释放锁的时候释放它,而是在你期望的时候释放它。作为一个你可能需要特别小心的领域,我只是提出了一个警告:)Kamarey,以下是Eric对这个主题的想法:我建议你使用一个结构,以避免GC开销。使用结构会导致装箱,因为该结构将实现IDisposable,这意味着对构造函数和Dispose方法的所有调用都将对该结构进行装箱/取消装箱,这将减慢主线程的速度,并导致一些箱子在GC堆上滞留,从而否定对结构的使用。给定实现IDisposable的结构/值类型的using子句将