views:

132

answers:

3

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?

+2  A: 

Well, it seems okay to me. Eric Lippert has previously written about the dangers of using Dispose for "non-resource" scenarios, but I think this would count as a resource.

It may make life tricky in upgrade scenarios, but you could always fall back to a more manual bit of code at that point.

Another alternative is to write a single lock acquire/use/release method and provide the action to take while holding the lock as a delegate.

Jon Skeet
Would you care to elaborate on the upgrade scenarios and why using this class might make it trickier than an alternative approach? I like the sound of using a method with a delegate, especially as it separates out the "action" part of my code from the "thread safety" parts.
Programming Hero
@Jon Skeet: Can you please provide a link to the Eric Lippert's post? I was unable to find it. Thanks.
Kamarey
@Kamarey: It was in an answer here on Stack Overflow, but it would take a while to find it I'm afraid. @Programming Hero: With respect to upgrading locks, I think it's really just a case of being careful that you don't release the lock when you didn't mean to, but that you *do* release it all the times you expect to. I'm just sounding a note of caution as an area you might want to be particularly careful over :)
Jon Skeet
Kamarey, here's Eric's thoughts on the topic: http://stackoverflow.com/questions/1095438/bad-practice-non-canon-usage-of-cs-using-statement/1096302#1096302
SolutionYogi
@SolutionYogi: Big thanks
Kamarey
+1  A: 

I usually indulge into this kind of code-sugary confections!

Here's a variant that's a bit easier to read for the users, on top of your 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 
  }
}

I also recommend using a variant that takes an Action delegate that's invoked when the lock cannot be obtained for 10 seconds, which I'll leave as an exercise to the reader.

You might also want to check for a null RWL in the static extension methods, and make sure the lock exists when you dispose it.

Cheers, Florian

Florian Doyon
I know it's only a demo, but this isn't thread-safe at all. Your `RWLSlim` is a local, meaning that every call will use a different lock instance.
LukeH
True, I will move it out of the locals, I initially didn't plan on writing a long method, sorry for the confusion
Florian Doyon
A: 

There is another consideration here, you are possibly solving a problem you should not solve. I can't see the rest of your code but I can guess from you seeing value in this pattern.

Your approach solves a problem only if the code that reads or writes the shared resource throws an exception. Implicit is that you don't handle the exception in the method that does the reading/writing. If you did, you could simply release the lock in the exception handling code. If you don't handle the exception at all, the thread will die from the unhandled exception and your program will terminate. No point in releasing a lock then.

So there's a catch clause somewhere lower in the call stack that catches the exception and handles it. This code has to restore the state of the program so that it can meaningful continue without generating bad data or otherwise die due to exceptions caused by altered state. That code has a difficult job to do. It needs to somehow guess how much data was read or written without having any context. Getting it wrong, or only partly right, is potentially very destabilizing to the entire program. After all, it was a shared resource, other threads are reading/writing from it.

If you know how to do this, then by all means use this pattern. You better test the heck out of though. If you're not sure then avoid wasting system resources on a problem you can't reliably fix.

Hans Passant
Although I don't need the try-finally blocks for logical processes, it does let me know that a ThreadAbortException will release all locks, should the thread be aborted. Moreover, the pattern is convenient, clear and unobtrusive.
Programming Hero
You can't safely abort a thread that is using a shared resource unless you abort *all* threads that use that resource. The state of your RWLS now no longer matters.
Hans Passant
Even true if the thread is aborting itself, i.e. it happens at a predictable moment, not in the middle of another operation?
Programming Hero
No. But then there is no point in protecting the RWLS since it could never have a lock active for that thread.
Hans Passant