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?

Paul Turner
  • 38,949
  • 15
  • 102
  • 166
  • I would suggest you use a struct instead, to avoid GC-overhead. – Lasse V. Karlsen Dec 14 '09 at 11:48
  • 2
    Using a struct will cause boxing as the struct will implement IDisposable, meaning all calls to the constructor and the Dispose method will box/unbox the struct, which will slow down the main thread and also cause some boxes to linger on the GC heap, thus negating the usage of a struct. – Florian Doyon Dec 14 '09 at 11:54
  • 2
    A using-clause that is given a struct/value type that implements IDisposable will not box the value, but call Dispose directly on the struct instead, which will work as intended. If there is a slight chance he might pass the section value around, then a struct is unusable, but if the only usage is for such a using-block, then a struct will avoid GC-overhead and work as suggested. – Lasse V. Karlsen Dec 14 '09 at 13:17
  • 1
    Of course, if you explicitly cast it to IDisposable, or return it from a SectionFactory-type-method as IDisposable, then it will be boxed. I suggest you return and/or declare it as the value type directly, this will avoid boxing and aliasing. In other words, the code as presented in the bottom of the question will work if ReaderWriterLockSection is a struct. – Lasse V. Karlsen Dec 14 '09 at 13:18
  • 1
    See http://billrob.com/archive/2006/05/12/530.aspx for details (the comment by Eric Lippert.) – Lasse V. Karlsen Dec 14 '09 at 13:21
  • I stand corrected, thanks Lasse! – Florian Doyon Dec 14 '09 at 15:55

4 Answers4

3

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
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 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. – Paul Turner Dec 14 '09 at 11:18
  • @Jon Skeet: Can you please provide a link to the Eric Lippert's post? I was unable to find it. Thanks. – Kamarey Dec 14 '09 at 11:56
  • @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 Dec 14 '09 at 14:04
  • 1
    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 Dec 15 '09 at 00:00
1

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
  • 4,146
  • 1
  • 27
  • 37
  • 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 Dec 14 '09 at 12:17
  • 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 Dec 14 '09 at 12:45
1

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
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 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. – Paul Turner Dec 14 '09 at 14:38
  • 1
    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 Dec 14 '09 at 14:42
  • Even true if the thread is aborting itself, i.e. it happens at a predictable moment, not in the middle of another operation? – Paul Turner Dec 14 '09 at 19:43
  • No. But then there is no point in protecting the RWLS since it could never have a lock active for that thread. – Hans Passant Dec 14 '09 at 19:51
  • If something bad happens by code that holds a writer-lock, I can understand presuming that the resource being protected might be in a bad state. But how could a holder of a reader lock corrupt the state of the shared resource? – supercat Aug 09 '11 at 21:50
  • Something didn't get read that was supposed to be read. Bad. – Hans Passant Aug 09 '11 at 22:01
1

One thing I'd suggest when wrapping a lock to facilitate the "using" pattern is to include a "danger-state" field in the lock; before allowing any code to enter the lock, the code should check the danger state. If the danger state is set, and the code which is trying to enter the lock hasn't passed a special parameter saying it's expecting that it might be, the attempt to acquire the lock should throw an exception. Code which is going to temporarily put the guarded resource into a bad state should set the danger state flag, do what needs to be done, and then reset the danger state flag once the operation is complete and the object is in a safe state.

If an exception occurs while the danger state flag is set, the lock should be released but the danger state flag should remain set. This will ensure that code which wants to access the resource will find out that the resource is corrupted, rather than waiting forever for the lock to be released (which would be the outcome if there were no "using" or "try-finally" block).

If the lock being wrapped is a ReaderWriterLock, it may be convenient to have the acquisition of a "writer" lock automatically set the danger state; unfortunately, there's no way for an IDisposable used by a using block to determine whether the block is being exited cleanly or via exception. Consequently, I don't know any way to use something syntactically like a 'using' block to guard the "danger state" flag.

supercat
  • 77,689
  • 9
  • 166
  • 211