ReaderWriterLock, Dispose pattern can also increase reliability

[Update 29/07/2004: Just updated the code, the old one was... broken to say the least :)]

Following Ian’s snippet about multithreading, and although I love the lock keyword as much as everybody, a Monitor is in my experience nearly always a bad choice. Why would you ask? What kind of weird thing the good old technologist have against the Monitor?

Well most of the time what you really want is let several threads read, but only one thread only write. Oh, and you don’t want to let someone write while you’re reading, nor do you want anyone reading while you write.

Let’s have a very quick overview of the ReaderWriterLock object. A ReaderWriterLock is a synchronization object that lets several readers acquire a reader lock at the same time. If any reader lock has been acquired, other reader locks will happily be given, but a request for a writer lock won’t be given until all the reader locks are released.

Not so complex you would tell me, and you wouldn’t be wrong. But to be really honest, the api for this object is cluttered, too complex, and as such, the code using it becomes very unreadable… And multi threaded unreadable code is an absolute no-go.

Let’s see what the methods are:

  • AcquireReaderLock to acquire a reader lock and provide a TimeSpan as a timeout value.
  • AcquireWriterLock to acquire a writer lock, and provide a TimeSpan as a timeout value.
  • ReleaseReaderLock to release a reader lock.
  • ReleaseWriterLock to release a writer lock.


Quite simple isn’t it? Now let’s see a few others as well:

  • UpgradeToWriterLock gets tricky, it lets you switch from your reader lock to a writer lock (and of course only if you’re the only reader does it return immediately)
  • DowngradeFromWriterLock let’s you release this upgrade.
  • ReleaseLock, RestoreLock you don’t really want to know about them, won’t need them, they are evil because they don’t timeout. If you want to know more, msdn is your friend.


So why exactly can’t I just AcquireWriterLock when I already have AcquireReaderLock? Remember that I said the ReaderWriterLock doesn’t let you acquire a writer lock unless all reader locks have been released… Which is why you have a different path! So my typical code would look like that:


            private ReaderWriterLock myLock;
            private Hashtable myObjectCache = new Hashtable();
            public void DoSomethingWithALock()
            {
                  myLock.AcquireReaderLock(TimeSpan.FromSeconds(30));
                  try
                  {
                        // let's see if i have this object in cache
                        StrongTypedObject myObject = myObjectCache["noItHasNothingToDoWithAKey"] as StrongTypedObject;

                        if (myObject == null)
                        {
                              // if not i'll load it
                              StrongTypedObject myNewObject = new StrongTypedObject("orWithChains");
                              // and now I need a write access...
                              LockCookie myCookie = myLock.UpgradeToWriterLock(TimeSpan.FromMinutes(1));
                              try
                              {
                                    myObjectCache["noItHasNothingToDoWithAKey"] = myNewObject;
                              }
                              finally
                              {
                                    myLock.DowngradeFromWriterLock(ref myCookie);
                              }
                        }
                  }
                  finally
                  {
                        myLock.ReleaseReaderLock();
                  }
            }



Ok so as you can see, the code is a bit complex. Now the question is, what happens if I want to factor out the creation of the object in a separate method? That’s where it becomes a real challenge. If I do that, I can either put in this new method the lock upgrade, or leave it to the caller to set it up. What is the problem with the lock upgrade being integrated with the method?

Well this method will be called from different points in your code, or worse, in other people code. You then require the caller to set up the correct lock environment (acquiring a reader lock) before calling your method. Not a good thingTM.

The best way to go is obviously to detect if when called, the method is already under a reader lock or not. This is the code doing it:

            public void CreateMyDoSomethingObject()
            {
                  bool isReaderLockHeldOnEntry = myLock.IsReaderLockHeld;
                  LockCookie myCookie = null;
                  if (isReaderLockHeldOnEntry)
                        myCookie = myLock.UpgradeToWriterLock(TimeSpan.FromSeconds(30));
                  else
                        myLock.AcquireWriterLock(TimeSpan.FromSeconds(30));

                  try
                  {
                        StrongTypedObject myNewObject = new StrongTypedObject("orWithChains");
                        myObjectCache["noItHasNothingToDoWithAKey"] = myNewObject;
                  }
                  finally
                  {
                        if (isReaderLockHeldOnEntry)
                              myLock.DowngradeFromWriterLock(ref myCookie);                                 else
                              myLock.ReleaseWriterLock();
                  }
            }

It’s already nearly unreadable. Now try to have several locks, and you end up with a very complex thing that is not readable by mortal people anymore.

Which is why I implemented the RWLock object following Ian’s code. With it, you can rewrite the code as:


            public void CreateMyDoSomethingObject()
            {
                  using (RWLock.LockWriter(myLock, TimeSpan.FromMinutes(1)))
                  {
                        StrongTypedObject myNewObject = new StrongTypedObject("orWithChains");
                        myObjectCache["noItHasNothingToDoWithAKey"] = myNewObject;
                  }
            }



As you can see, it’s much much less cluttered. The LockWriter method either upgrade to a writer lock or acquire a new writer lock, depending on the state of the lock on the current thread when the method was called. Now you can ensure good synchronization, and neither have to have a lot of code or leave the caller to deal himself with setting the locks right.

The full code for the class (also attached as a .zip file)

using System;

using System.Threading;

// (C) 2004 Sebastien Lambla

// Unlimited use license given to Cubiks Ltd for the Cubiks Online platform.

namespace Cubiks.Threading

{

#if DEBUG

      public class RWLock : IDisposable

#else

public struct RWLock : IDisposable

#endif

      {

            private static TimeSpan timeout = TimeSpan.FromSeconds(5);

 

            public static RWLock LockReader(ReaderWriterLock lockObject)

            {

                  return LockReader (lockObject, timeout);

            }

 

            public static RWLock LockReader(ReaderWriterLock o, TimeSpan timeout)

            {

                  RWLock tl = new RWLock(o);

                  try

                  {

                        o.AcquireReaderLock(timeout);

                  }

                  catch (ApplicationException e)

                  {

#if DEBUG

                        System.GC.SuppressFinalize(tl);

#endif

                        throw;

                  }

                  return tl;

            }

            public static RWLock LockWriter(ReaderWriterLock o)

            {

                  return LockWriter(o, timeout);

            }

            public static RWLock LockWriter(ReaderWriterLock o, TimeSpan timeout)

            {

                  RWLock tl = new RWLock(o);

           

                  try

                  {

                        if (o.IsReaderLockHeld)

                        {

                              tl.lockCookie = o.UpgradeToWriterLock(timeout);

                              tl.lockType = LockType.WriterFromReader;

                        }

                        else

                        {

                              o.AcquireWriterLock(timeout);

                              tl.lockType = LockType.Writer;

                        }

                  }

                  catch (ApplicationException)

                  {

#if DEBUG

                        System.GC.SuppressFinalize(tl);

#endif

                        throw;

                  }

                  return tl;

            }

 

            private ReaderWriterLock target;

            private LockType lockType;

            private LockCookie lockCookie;

            private RWLock (ReaderWriterLock o)

            {

                  lockType = LockType.None;

                  lockCookie = new LockCookie();

                  target = o;

            }

 

            public void Dispose ()

            {

           

                  try

                  {

                        switch (this.lockType)

                        {

                              case LockType.Reader:

                              {

                                    target.ReleaseReaderLock();

                                    break;

                              }

                              case LockType.Writer:

                              {

                                    target.ReleaseWriterLock();

                                    break;

                              }

                              case LockType.WriterFromReader:

                              {

                                    target.DowngradeFromWriterLock(ref lockCookie);

                                    break;

                              }

                              default:

                                    System.Diagnostics.Debug.WriteLine("Problem, no lock acquired!");

                                    break;

                        }

                  }

                  catch (ApplicationException exception)

                  {

                        System.Diagnostics.Debug.WriteLine(exception.ToString());

                  }

                  catch (Exception e)

                  {

                        System.Diagnostics.Debug.WriteLine(e.ToString());

                  }

                  // It's a bad error if someone forgets to call Dispose,

                  // so in Debug builds, we put a finalizer in to detect

                  // the error. If Dispose is called, we suppress the

                  // finalizer.

#if DEBUG

                  GC.SuppressFinalize(this);

#endif

            }

 

#if DEBUG

            ~RWLock()

            {

                  // If this finalizer runs, someone somewhere failed to

                  // call Dispose, which means we've failed to leave

                  // a monitor!

                  System.Diagnostics.Debug.Fail("Undisposed lock");

            }

#endif

            private enum LockType

            {

                  None,

                  Reader,

                  Writer,

                  WriterFromReader

            }

      }

}



Ads