Silverlight ReaderWriterLock Implementation

Posted by Rishi on 08-Mar-09 5:20 AM - Comments (4)

One of the many things missing from the Silverlight API is a ReaderWriterLock implementation, though it is not a day-to-day requisite yet sometimes you really need it. The good news is, the underpinnings for it's implementation are all their, basically carried in-tact from the desktop counterpart.

I found a very good implementation by Vance Morrison, which I understand forms the basis of the ReaderWriterLockSlim in .NET 3.5 using spin locks.  Below is the Sliverlight version of the same, with some minute changes:

/// <summary>
/// A reader-writer lock implementation that is intended to be simple, yet very
/// efficient.  In particular only 1 interlocked operation is taken for any lock 
/// operation (we use spin locks to achieve this).  The spin lock is never held
/// for more than a few instructions (in particular, we never call event APIs
/// or in fact any non-trivial API while holding the spin lock).   
/// 
/// Currently this ReaderWriterLock does not support recurision, however it is 
/// not hard to add 
/// </summary>
/// <remarks>
/// By Vance Morrison
/// Taken from - http://blogs.msdn.com/vancem/archive/2006/03/28/563180.aspx
/// Code at - http://blogs.msdn.com/vancem/attachment/563180.ashx
/// </remarks>
public class ReaderWriterLock
{
    // Lock specifiation for myLock:  This lock protects exactly the local fields associted
    // instance of MyReaderWriterLock.  It does NOT protect the memory associted with the
    // the events that hang off this lock (eg writeEvent, readEvent upgradeEvent).
    int myLock;

    // Who owns the lock owners > 0 => readers
    // owners = -1 means there is one writer.  Owners must be >= -1.  
    int owners;

    // These variables allow use to avoid Setting events (which is expensive) if we don't have to. 
    uint numWriteWaiters;        // maximum number of threads that can be doing a WaitOne on the writeEvent 
    uint numReadWaiters;         // maximum number of threads that can be doing a WaitOne on the readEvent
    uint numUpgradeWaiters;      // maximum number of threads that can be doing a WaitOne on the upgradeEvent (at most 1). 

    // conditions we wait on. 
    EventWaitHandle writeEvent;    // threads waiting to aquire a write lock go here.
    EventWaitHandle readEvent;     // threads waiting to aquire a read lock go here (will be released in bulk)
    EventWaitHandle upgradeEvent;  // thread waiting to upgrade a read lock to a write lock go here (at most one)

    public ReaderWriterLock()
    {
        // All state can start out zeroed. 
    }

    public void AcquireReaderLock(int millisecondsTimeout)
    {
        EnterMyLock();
        for (; ; )
        {
            // We can enter a read lock if there are only read-locks have been given out
            // and a writer is not trying to get in.  
            if (owners >= 0 && numWriteWaiters == 0)
            {
                // Good case, there is no contention, we are basically done
                owners++;       // Indicate we have another reader
                break;
            }

            // Drat, we need to wait.  Mark that we have waiters and wait.  
            if (readEvent == null)      // Create the needed event 
            {
                LazyCreateEvent(ref readEvent, false);
                continue;   // since we left the lock, start over. 
            }

            WaitOnEvent(readEvent, ref numReadWaiters, millisecondsTimeout);
        }
        ExitMyLock();
    }

    public void AcquireWriterLock(int millisecondsTimeout)
    {
        EnterMyLock();
        for (; ; )
        {
            if (owners == 0)
            {
                // Good case, there is no contention, we are basically done
                owners = -1;    // indicate we have a writer.
                break;
            }

            // Drat, we need to wait.  Mark that we have waiters and wait.
            if (writeEvent == null)     // create the needed event.
            {
                LazyCreateEvent(ref writeEvent, true);
                continue;   // since we left the lock, start over. 
            }

            WaitOnEvent(writeEvent, ref numWriteWaiters, millisecondsTimeout);
        }
        ExitMyLock();
    }

    public void UpgradeToWriterLock(int millisecondsTimeout)
    {
        EnterMyLock();
        for (; ; )
        {
            Debug.Assert(owners > 0, "Upgrading when no reader lock held");
            if (owners == 1)
            {
                // Good case, there is no contention, we are basically done
                owners = -1;    // inidicate we have a writer. 
                break;
            }

            // Drat, we need to wait.  Mark that we have waiters and wait. 
            if (upgradeEvent == null)   // Create the needed event
            {
                LazyCreateEvent(ref upgradeEvent, false);
                continue;   // since we left the lock, start over. 
            }

            if (numUpgradeWaiters > 0)
            {
                ExitMyLock();
                throw new InvalidOperationException("UpgradeToWriterLock already in process.  Deadlock!");
            }

            WaitOnEvent(upgradeEvent, ref numUpgradeWaiters, millisecondsTimeout);
        }
        ExitMyLock();
    }

    public void ReleaseReaderLock()
    {
        EnterMyLock();
        Debug.Assert(owners > 0, "ReleasingReaderLock: releasing lock and no read lock taken");
        --owners;
        ExitAndWakeUpAppropriateWaiters();
    }

    public void ReleaseWriterLock()
    {
        EnterMyLock();
        Debug.Assert(owners == -1, "Calling ReleaseWriterLock when no write lock is held");
        Debug.Assert(numUpgradeWaiters > 0);
        owners++;
        ExitAndWakeUpAppropriateWaiters();
    }

    public void DowngradeToReaderLock()
    {
        EnterMyLock();
        Debug.Assert(owners == -1, "Downgrading when no writer lock held");
        owners = 1;
        ExitAndWakeUpAppropriateWaiters();
    }

    /// <summary>
    /// A routine for lazily creating a event outside the lock (so if errors
    /// happen they are outside the lock and that we don't do much work
    /// while holding a spin lock).  If all goes well, reenter the lock and
    /// set 'waitEvent' 
    /// </summary>
    private void LazyCreateEvent(ref EventWaitHandle waitEvent, bool makeAutoResetEvent)
    {
        Debug.Assert(MyLockHeld);
        Debug.Assert(waitEvent == null);

        ExitMyLock();
        EventWaitHandle newEvent;
        if (makeAutoResetEvent)
            newEvent = new AutoResetEvent(false);
        else
            newEvent = new ManualResetEvent(false);
        EnterMyLock();
        if (waitEvent == null)          // maybe someone snuck in. 
            waitEvent = newEvent;
    }

    /// <summary>
    /// Waits on 'waitEvent' with a timeout of 'millisceondsTimeout.  
    /// Before the wait 'numWaiters' is incremented and is restored before leaving this routine.
    /// </summary>
    private void WaitOnEvent(EventWaitHandle waitEvent, ref uint numWaiters, int millisecondsTimeout)
    {
        Debug.Assert(MyLockHeld);

        waitEvent.Reset();
        numWaiters++;

        bool waitSuccessful = false;
        ExitMyLock();      // Do the wait outside of any lock 
        try
        {
            if (!waitEvent.WaitOne(millisecondsTimeout))
                throw new InvalidOperationException("ReaderWriterLock timeout expired");
            waitSuccessful = true;
        }
        finally
        {
            EnterMyLock();
            --numWaiters;
            if (!waitSuccessful)        // We are going to throw for some reason.  Exit myLock. 
                ExitMyLock();
        }
    }

    /// <summary>
    /// Determines the appropriate events to set, leaves the locks, and sets the events. 
    /// </summary>
    private void ExitAndWakeUpAppropriateWaiters()
    {
        Debug.Assert(MyLockHeld);

        if (owners == 0 && numWriteWaiters > 0)
        {
            ExitMyLock();      // Exit before signaling to improve efficiency (wakee will need the lock)
            writeEvent.Set();   // release one writer. 
        }
        else if (owners == 1 && numUpgradeWaiters != 0)
        {
            ExitMyLock();          // Exit before signaling to improve efficiency (wakee will need the lock)
            upgradeEvent.Set();     // release all upgraders (however there can be at most one). 
            // two threads upgrading is a guarenteed deadlock, so we throw in that case. 
        }
        else if (owners >= 0 && numReadWaiters != 0)
        {
            ExitMyLock();    // Exit before signaling to improve efficiency (wakee will need the lock)
            readEvent.Set();  // release all readers. 
        }
        else
            ExitMyLock();
    }

    private void EnterMyLock()
    {
        if (Interlocked.CompareExchange(ref myLock, 1, 0) != 0)
            EnterMyLockSpin();
    }

    private void EnterMyLockSpin()
    {
        for (int i = 0; ; i++)
        {
            if (i < 3 && Environment.ProcessorCount > 1)
                Thread.SpinWait(20);    // Wait a few dozen instructions to let another processor release lock. 
            else
                Thread.Sleep(0);        // Give up my quantum.  

            if (Interlocked.CompareExchange(ref myLock, 1, 0) == 0)
                return;
        }
    }
    private void ExitMyLock()
    {
        Debug.Assert(myLock != 0, "Exiting spin lock that is not held");
        myLock = 0;
    }

    private bool MyLockHeld { get { return myLock != 0; } }

}

This implementation features both timeouts and upgrading to writer locks, however it does not support recursion or try enter lock usage. Also, it's relatively fast, in some basic testings I did it ran 3x-4x v/s Monitor Locks, which compares well to other less feature-rich implementations (see here) that run 8x-10x v/s the generic Monitor Locks.  And just as a resource, you can read more about locks in general from this entry by Jeff Moser on his blog.

Comments (4) -

chadbr
chadbr United States
on 06-Feb-10 8:12 AM
I was trying to use this class today - but the writer logic seems to be flawed.

AcquireWriterLock() sets the owners to -1

73.            if (owners == 0)  
74.            {  
75.                // Good case, there is no contention, we are basically done  
76.                owners = -1;  

and the 1st thing the release does is assert on -1

131.    public void ReleaseWriterLock()  
132.    {  
133.        EnterMyLock();  
134.        Debug.Assert(owners == -1, "Calling ReleaseWriterLock when no write lock is held");  

Am I missing something?


Rishi
Rishi
on 07-Feb-10 11:27 AM
@chadbr, that assert on release is to ensure/check that you are not trying to release a writer lock when say you only have a reader lock.

Now, if I remember correctly this stuff is for Silverlight 2, which din't have the lock constructs - but now you have them, preferably use them. Also, for some reason I chose not to use this particular implementation in Silverlight 2, I used another available here:

nroute.codeplex.com/.../3807#105595

This ReaderWriterLock implementation has accompanying Disposable WriterLocks and Disposable ReaderLocks helper classes.

Hope that helps.

chadbr
chadbr United States
on 09-Feb-10 1:29 AM
I was missing something - sleep!

In fact the code was asserting on this line in ReleaseWriter:

Debug.Assert(numUpgradeWaiters > 0);

This helps a lot - thanks.

I'll take a look @ the nroute version... I'm porting a large desktop app to SL that uses a lot of Disposable RWL's. A SL version helps smooth the transition.

Thanks --

Rishi
Rishi
on 09-Feb-10 10:36 AM
@chadbr great, also if possible I would really like to hear how your use of the RWLs goes. And which of the two version you decided to use, and if you did any perf testing how did it go? Would appreciate that, cheers.

solar power systems
solar power systems Islamic Republic of Pakistan
on 02-Jan-12 5:48 PM
Interesting blog. its been great time reading your post. looking for other posts....

Wind Turbines
Wind Turbines Islamic Republic of Pakistan
on 02-Feb-12 11:30 PM
I found lots of interesting information here. The post was professionally written and I feel like the author has extensive knowledge in the subject. Keep it that way.
http://www.greenbiz.com.pk/wind-energy.php

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading