NobleTech Software Consultancy

Deadlock with ReaderWriterLockSlim and other lock objects

Introduction

I recently posted some code on a couple of blogs to show how to avoid a deadlock when using ReaderWriterLockSlim and other lock objects. This has led to a couple of questions about why this code is necessary so I thought I'd post more details here.

What's the problem?

Code like this is common, but can lead to a deadlock:

ReaderWriterLockSlim myLock = new ReaderWriterLockSlim();
myLock.EnterReadLock();
try
{
    // Do stuff
}
finally
{
    // Release the lock
    myLock.ExitReadLock();
}

What causes the deadlock?

Essentially it boils down to an exception being thrown between aquiring the lock and starting the try or using block. The call to EnterReadLock appears on the line before the try clause. If an exception occurs between the two statements then the finally code that releases the lock will never be called.

How can an exception be thrown where there's no code?

There's no code between EnterReadLock and try, so what could cause an exception? Well for one thing, ThreadAbortException could occur at any point in your code, not to mention exceptions related to external resources, etc. You might think ThreadAbortExceptions are so rare that they're not worth worrying about, but they can actually be quite common. When a browser of your website disconnects before the page has finished processing you may get an exception and if a web request times-out then you will definitely get an exception. It only takes one of these exceptions to occur between the lock acquire and the try block for your lock to be acquired permenantly, preventing any further writes to your protected resource.

What's the solution?

In the code above the solution is easy, acquire the lock in the try block as follows:

ReaderWriterLockSlim myLock = new ReaderWriterLockSlim();
try
{
    myLock.EnterReadLock();
    // Do stuff
}
finally
{
    // Release the lock
    myLock.ExitReadLock();
}

Are using blocks safe?

It is common to encapsulate the above pattern using a manager object that implements IDisposable to avoid repeatedly writing (and remembering to write) the lock release code. A typical example would be used like this:

using (new LockMgr(myLock))
{
    // Do stuff
}

The constructor for the lock manager would acquire the lock and the Dispose method would release the lock. Lovely and simple and very little code to write. But it deadlocks.

I have also seen one example of encapsulating this object creation in an extension method on the lock object itself. While this reduces typing still further it may also obscure what is happening. An interesting idea though.

How does the using block deadlock?

How can deadlock occur when the lock is acquired as part of the initialisation of the using statement? Well it's pretty much the same situation. The lock is acquired during the construction of the manager object. After this happens the created object has to be returned and then assigned to an (invisible) variable before the using block will take responsibility for disposing of it. If the exception occurs between the acquiring of the lock and the assignment of the returned manager object then the object is never disposed of. You can only guarantee that the object will be disposed of AFTER you have entered the code enclosed by the using block.

How can a using block be used safely?

Below I show the pattern I use for acquiring locks. It forces you to acquire the lock after the creation of the manager object. It's an extra line of code but it will buy you significant extra protection from deadlock.

Use this code as follows:

ReaderWriterLockSlim myLock = new ReaderWriterLockSlim();

using (ReaderWriterLockMgr lockMgr = new ReaderWriterLockMgr(myLock))
{
    lockMgr.EnterReadLock();
    // Do stuff
}

using (ReaderWriterLockMgr lockMgr1 = new ReaderWriterLockMgr(myLock))
{
    lockMgr1.EnterUpgradeableReadLock();
    // Do stuff
    using (ReaderWriterLockMgr lockMgr2 = new ReaderWriterLockMgr(myLock))
    {
        lockMgr2.EnterWriteLock();
        // Do stuff
    }
}

The code below is abbreviated, for the complete code with correct error handling and full comments as used in NobleConquest please download ReaderWriterLockMgr.cs.

public sealed class ReaderWriterLockMgr : IDisposable
{
    private ReaderWriterLockSlim _readerWriterLock = null;

    private enum LockTypes { None, Read, Write, Upgradeable }
    private LockTypes _enteredLockType = LockTypes.None;

    public ReaderWriterLockMgr(ReaderWriterLockSlim ReaderWriterLock)
    {
        _readerWriterLock = ReaderWriterLock;
    }

    public void EnterReadLock()
    {
        _readerWriterLock.EnterReadLock();
        _enteredLockType = LockTypes.Read;
    }

    public void EnterWriteLock()
    {
        _readerWriterLock.EnterWriteLock();
        _enteredLockType = LockTypes.Write;
    }

    public void EnterUpgradeableReadLock()
    {
        _readerWriterLock.EnterUpgradeableReadLock();
        _enteredLockType = LockTypes.Upgradeable;
    }

    public bool ExitLock()
    {
        switch (_enteredLockType)
        {
            case LockTypes.Read:
                _readerWriterLock.ExitReadLock();
                _enteredLockType = LockTypes.None;
                return true;
            case LockTypes.Write:
                _readerWriterLock.ExitWriteLock();
                _enteredLockType = LockTypes.None;
                return true;
            case LockTypes.Upgradeable:
                _readerWriterLock.ExitUpgradeableReadLock();
                _enteredLockType = LockTypes.None;
                return true;
        }
        return false;
    }

    public void Dispose()
    {
        ExitLock();
    }

    protected ~ReaderWriterLockMgr()
    {
        ExitLock();
    }
}

Questions?

As always, any questions, comments or improvements to this article are welcomed. Please contact me using the company's contact details.

Hope this helped!
Nathan Phillips.

This article last updated: 08/03/2010