.NET Article        
Skip Navigation Links
Home
Articles
Code
Applications
Bookshelf
CV/Resume

ReaderWriterLock Helper Class

Download code for this article:
ReaderWriterLockHelper.zip (1KB)

The ReaderWriterLock is a very useful synchronisation lock, designed for situations where a resource is read from often, but modified relatively infrequently. When compared to using a simpler lock, it allows for greater efficiency, since it allows many readers to read from a shared resource, but only one writer to write to it.

There is a danger of deadlock, however, if the current thread already owns a reader lock and attempts to acquire a writer lock. If you set an infinite timeout on the AcquireWriterLock attempt, the thread will deadlock, waiting for ever for the Reader lock to be released.

This can happen quite easily. For example, suppose we have a resource, ImportantInfo, which is read from a file into memory - and let's suppose that, since it takes a while to read from the file, we only want to do this once. However, when the file changes, an event needs to fire, allowing us to read it again.

Let's attempt to write a class, ImportantInfoProvider, which allows for this scenario. If you take a look at the first attempt below, you can see the problem:

public class ImportantInfoProvider
{
    private static ImportantInfo _info;    
    private static ReaderWriterLock _lock = new ReaderWriterLock();

    private static ImportantInfo Info
    {
        get
        {
            // false indicates don't read if already read
            ReadImportantInfo(false);
            return _info;
        }
    }

    private static ImportantInfo_FileChanged(
        object sender, 
        FileChangedEventArgs e
        )
    {
        // imagine this event handler has been wired up to fire whenever 
        // the ImportantInfo.config file changes
        

        // true indicates read it fresh, regardless if a previous version 
        // has been read and is already in memory
        ReadImportantInfo(true);
    }

    
    public static string GetImportantInfoItem(string nameOfItem)
    {
        _lock.AcquireReaderLock(Timeout.Infinite);
        try
        {
            // assume ImportantInfo has an indexer returning info based 
            // on a string key
            return Info[nameOfItem];
        }
        finally
        {
            _lock.ReleaseReaderLock();
        }
    }

    private static void ReadImportantInfo(bool bForceReplace)
    {
        // if its already there, and not replacing, then return
        if (!bForceReplace && _info != null)
        {
            return;
        }
            
        _lock.AcquireWriterLock(Timeout.Infinite);
        try
        {
            // another check to see if its there, after getting the lock,
            // (in case its already been done by another thread)
            if (!bForceReplace && _info != null)
            {
                return;
            }


            _info = ReadImportantInfoFromInfoFile();

        }
        finally
        {
            _lock.ReleaseWriterLock();
        }
    }
}

The problem with this code is primarily because of the lazy load in the Info property - the first time the GetImportantInfoItem function is called, the Info property is dereferenced. This causes ReadImportantInfo to be called. Note that the thread has acquired the read lock before dereferencing Info.

Since the _info object has not been populated from the file yet, ReadImportantInfo goes ahead and tries to acquire the writer lock, so that it can safely write the contents of the ImportantInfo file into the _info object. However, since the reader lock has already been obtained, that's a deadlock. The ReaderWriterLock does not allow a writer lock to be obtained if the thread already owns a writer lock.

The ReaderWriterLock does however provide enough functionality to get around this - there is an IsReaderLockHeld property, and an UpgradeToWriterLock method, which you can call if you already own a reader lock. (You would also need to call DowngradeFromWriterLock if you had upgraded, to return to just owning a reader lock).

The way in which these functions are used follows a fairly generic pattern - in other words you would find yourself writing the same code time and time again when you have scenarios like the one described. Therefore it would be nice to be able to just try to acquire a writer lock, and to have it automatically determined whether you need to upgrade to a writer lock (because you already have the reader lock) or whether you can just acquire it as normal.

I've written a simple helper class, ReaderWriteLockHelper, to do this. I'll show how the above code is corrected using the class, and then show the helper class itself.

The key problem is the call to AcquireWriterLock, in ReadImportantInfo - this is where we want some automatic determination of whether to upgrade or just acquire. So, an instance of the ReaderWriterLockHelper class is used to do exactly this:

    private static void ReadImportantInfo(bool bForceReplace)
    {
        // if its already there, and not replacing, then return
        if (!bForceReplace && _info != null)
        {
            return;
        }
            

/***/    ReaderWriterLockHelper rwLockHelper = 
            new ReaderWriterLockHelper(_lock);
            
/***/    LockCookie lockCookie = rwLockHelper.AcquireWriterLock();

        try
        {
            // another check to see if its there, after getting the lock,
            // (in case its already been done by another thread)
            if (!bForceReplace && _info != null)
            {
                return;
            }


            _info = ReadImportantInfoFromInfoFile();

        }
        finally
        {
/***/        rwLockHelper.ReleaseWriterLock(ref lockCookie);
        }

    }

(The LockCookie object is obtained when upgrading and passed in when downgrading, in order to somehow keep track of some information necessary for the downgrading process.)


The ReaderWriterLockHelper class itself is as follows:
// ReaderWriterLockHelper

using System;
using System.Threading;

namespace Beech.Util
{
    /// <summary>
    /// Summary description for ReaderWriterLockExt.
    /// </summary>
    public class ReaderWriterLockHelper
    {
        private ReaderWriterLock _rwLock;
        private bool _bWasReaderLockHeld = false;

        public ReaderWriterLockHelper(ReaderWriterLock rwLock)
        {
            _rwLock = rwLock;
        }

        public LockCookie AcquireWriterLock()
        {
            return ReaderWriterLockHelper.AcquireWriterLock(
                _rwLock, 
                out _bWasReaderLockHeld
                );
        }

        public void ReleaseWriterLock(ref LockCookie lockCookie)
        {
            ReaderWriterLockHelper.ReleaseWriterLock(
                _rwLock, 
                _bWasReaderLockHeld, 
                ref lockCookie
                );
        }

        /// <summary>
        /// Acquires the writers lock, or upgrades to a writers lock if a 
        /// readers lock was held previously
        /// </summary>
        /// <param name="bWasReaderLockHeld"></param>
        /// <returns></returns>
        /// <remarks>to avoid deadlock, need to check if reader lock is 
        /// currently held before attempting to gain writer lock</remarks>
        public static LockCookie AcquireWriterLock(
            ReaderWriterLock rwLock, 
            out bool bWasReaderLockHeld
            )
        {
            if (rwLock == null)
            {
                bWasReaderLockHeld = false;
                // can't return null..
                return new LockCookie();
            }

            LockCookie lockCookie = new LockCookie();
            bWasReaderLockHeld = rwLock.IsReaderLockHeld;
            if (bWasReaderLockHeld)
            {
                lockCookie = rwLock.UpgradeToWriterLock(Timeout.Infinite);
            }
            else
            {
                rwLock.AcquireWriterLock(Timeout.Infinite);
            }

            return lockCookie;
        }

        /// <summary>
        /// Releases Writer Lock, with consideration to whether reader lock 
        /// was held before, in which case a downgrade to reader lock is 
        /// actually performed.
        /// </summary>
        /// <param name="bWasReaderLockHeld"></param>
        /// <param name="lockCookie"></param>
        /// <remarks>to avoid deadlock, need to check if reader lock is 
        /// currently held before attempting to gain writer lock - this 
        /// function performs the release of the writer lock, according to 
        /// whether reader lock was held previously</remarks>
        public static void ReleaseWriterLock(
            ReaderWriterLock rwLock, 
            bool bWasReaderLockHeld, 
            ref LockCookie lockCookie
            )
        {
            if (rwLock == null)
            {
                return;
            }

            if (bWasReaderLockHeld)
            {
                rwLock.DowngradeFromWriterLock(ref lockCookie);
            }
            else
            {
                rwLock.ReleaseWriterLock();
            }
        }

    }
}

By using this class whenever you use a ReaderWriterLock you can avoid possible deadlocks, and save yourself writing similar code each time.

(In case you're wondering in the reverse situation, i.e. you have a writer lock, and call a function which acquires a reader lock - this is OK, and will work fine. What happens is that the attempt to acquire a reader lock will actually cause another writer lock to be obtained (i.e. the lock count is incremented). When the call to ReleaseReaderLock comes, the documentation states that "If a thread has the writer lock, calling ReleaseReaderLock has the same effect as calling ReleaseWriterLock." - so an AcquireReaderLock and ReleaseReaderLock will do the right thing, even if a writer lock is held when they're called.)

© 2006 Pete Beech