4

In writing some threaded code, I've been using the ReaderWriterLockSlim class to handle synchronized access to variables. Doing this, I noticed I was always writing try-finally blocks, the same for each method and property.

Seeing an opportunity to avoid repeating myself and encapsulate this behaviour I built a class, ReaderWriterLockSection, intended to be used as a thin wrapper to the lock which can be used with the C# using block syntax.

The class is mostly as follows:

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.
   }
}

I use the section as follows:

private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

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

To me, this seems like a good idea, one that makes my code easier to read and seemingly more robust since I wont ever forget to release a lock.

Can anybody see an issue with this approach? Is there any reason this is a bad idea?

4

4 回答 4

3

嗯,对我来说似乎没问题。Eric Lippert 之前写过关于使用Dispose“非资源”场景的危险,但我认为这将算作一种资源。

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

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

于 2009-12-14T11:06:58.277 回答
1

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

这是一个更易于用户阅读的变体,位于您的 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,并确保在释放锁时存在锁。

干杯,弗洛里安

于 2009-12-14T11:39:37.913 回答
1

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

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

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

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

于 2009-12-14T13:41:33.223 回答
1

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

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

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

于 2012-03-04T00:04:36.710 回答