Click here to Skip to main content
Click here to Skip to main content

ReaderWriterSlimLock: Conquering Mismatched Enter and Exit Calls

By , 26 Nov 2012
 

The ReaderWriterLockSlim class is used to protect a resource that is read by multiple threads and written to by one thread at a time. The class exists in the desktop FCL, but is noticeably absent from the Silverlight FCL. To readily support both platforms, some time ago, I incorporated the open-source mono implementation of the ReaderWriterLockSlim class into the Silverlight version of my core library, which is downloadable from http://calcium.codeplex.com/SourceControl/list/changesets

ReaderWriterLockSlim is a light-weight alternative to the Monitor class, for providing for concurrency. I say light-weight because the Monitor class only provides for exclusive access; where only a single thread can enter, regardless of the kind of operation.

The Monitor class, however, has an advantage: its syntax can be simplified using the lock statement, as shown in the following code snippet:

System.Object resourceLock = new System.Object();
System.Threading.Monitor.Enter(resourceLock);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(resourceLock);
}

Which is equivalent to the following, when using a lock statement:

lock (x)
{
    DoSomething();
}

Using the lock statement means that we never get caught out forgetting to exit the lock, which could lead to a thread being blocked indefinitely.

No such baked-in infrastructure exists for the ReaderWriterSlimLock. So, I've created a number of extension methods to simulate the lock statement for the ReaderWriterLockSlim.

Using Extension Methods to Simulate Lock Syntax

If you've used the ReaderWriterSlimLock a lot, you'll know that mismatching Enter and Exit calls can be easy to do, especially when pasting code. For example, in the following excerpt we see how a call to enter a write protected section of code is mismatched with a call to exit a read section:

ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim();

try
{
    lockSlim.EnterWriteLock();
    DoSomething();
}
finally
{
    lockSlim.ExitReadLock();
}

This code will result in a System.Threading. SynchronizationLockException being raised.

The extension methods that have been written, allow a Func or an Action to be supplied, which will be performed within a try/finally block. The previous excerpt can be rewritten more concisely as:

ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim();
lockSlim.PerformUsingWriteLock(() => DoSomething);

In the downloadable code I have included a unit test to demonstrate how the ReaderWriterLockSlim extension methods are used, (see Listing 1).

Listing 1: LockSlimTests Class

[TestClass]
public class LockSlimTests : SilverlightTest
{
    readonly static List<string> sharedList = new List<string>();
 
    [TestMethod]
    public void ExtensionsShouldPerformActions()
    {
        ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim();
 
        string item1 = "test";
 
        //    Rather than this code:
        //            try
        //            {
        //                lockSlim.EnterWriteLock();
        //                sharedList.Add(item1);
        //            }
        //            finally
        //            {
        //                lockSlim.ExitWriteLock();
        //            }
        //    We can write this one liner:
        lockSlim.PerformUsingWriteLock(() => sharedList.Add(item1));
 
        //    Rather than this code:
        //            string result;
        //            try
        //            {
        //                lockSlim.EnterReadLock();
        //                result = sharedList[0];
        //            }
        //            finally
        //            {
        //                lockSlim.ExitReadLock();
        //            }
        //    We can write this one liner:
        string result = lockSlim.PerformUsingReadLock(() => sharedList[0]);
 
        Assert.AreEqual(item1, result);
 
        string item2 = "test2";
 
        //    Rather than this code:
        //            try
        //            {
        //                lockSlim.EnterUpgradeableReadLock();
        //                if (!sharedList.Contains(item2))
        //                {
        //                    try
        //                    {
        //                        lockSlim.EnterWriteLock();
        //                        sharedList.Add(item2);
        //                    }
        //                    finally
        //                    {
        //                        lockSlim.ExitWriteLock();
        //                    }
        //                }
        //            }
        //            finally
        //            {
        //                lockSlim.ExitUpgradeableReadLock();
        //            }
        //    We can write this:
        lockSlim.PerformUsingUpgradeableReadLock(() => 
            {
                if (!sharedList.Contains(item2))
                {
                    lockSlim.PerformUsingWriteLock(() => sharedList.Add(item2));
                }
            });
 
        //    Rather than this code:
        //            try
        //            {
        //                lockSlim.EnterReadLock();
        //                result = sharedList[1];
        //            }
        //            finally
        //            {
        //                lockSlim.ExitReadLock();
        //            }
        //    We can write this:
        result = lockSlim.PerformUsingReadLock(() => sharedList[1]);
 
        Assert.AreEqual(item2, result);
    }
 
}

The result of executing this test is shown in Figure 1.

Figure 1: Result of running the ReaderWriterLockSlimExtension tests.

The ReaderWriterLockSlimExtensions accept a simple Action or a Func to return value, and take care of calling the appropriate Enter and Exit methods, inside a try/finally block, (see Listing 2).

Listing 2: ReaderWriterLockSlimExtensions Class

public static class ReaderWriterLockSlimExtensions
{
    public static void PerformUsingReadLock(this ReaderWriterLockSlim readerWriterLockSlim, Action action)
    {
        ArgumentValidator.AssertNotNull(readerWriterLockSlim, "readerWriterLockSlim");
        ArgumentValidator.AssertNotNull(action, "action");
        try
        {
            readerWriterLockSlim.EnterReadLock();
            action();
        }
        finally
        {
            readerWriterLockSlim.ExitReadLock();
        }
    }
 
    public static T PerformUsingReadLock<T>(this ReaderWriterLockSlim readerWriterLockSlim, Func<T> action)
    {
        ArgumentValidator.AssertNotNull(readerWriterLockSlim, "readerWriterLockSlim");
        ArgumentValidator.AssertNotNull(action, "action");
        try
        {
            readerWriterLockSlim.EnterReadLock();
            return action();
        }
        finally
        {
            readerWriterLockSlim.ExitReadLock();
        }
    }
 
    public static void PerformUsingWriteLock(this ReaderWriterLockSlim readerWriterLockSlim, Action action)
    {
        ArgumentValidator.AssertNotNull(readerWriterLockSlim, "readerWriterLockSlim");
        ArgumentValidator.AssertNotNull(action, "action");
        try
        {
            readerWriterLockSlim.EnterWriteLock();
            action();
        }
        finally
        {
            readerWriterLockSlim.ExitWriteLock();
        }
    }
 
    public static T PerformUsingWriteLock<T>(this ReaderWriterLockSlim readerWriterLockSlim, Func<T> action)
    {
        ArgumentValidator.AssertNotNull(readerWriterLockSlim, "readerWriterLockSlim");
        ArgumentValidator.AssertNotNull(action, "action");
        try
        {
            readerWriterLockSlim.EnterWriteLock();
            return action();
        }
        finally
        {
            readerWriterLockSlim.ExitWriteLock();
        }
    }
 
    public static void PerformUsingUpgradeableReadLock(this ReaderWriterLockSlim readerWriterLockSlim, Action action)
    {
        ArgumentValidator.AssertNotNull(readerWriterLockSlim, "readerWriterLockSlim");
        ArgumentValidator.AssertNotNull(action, "action");
        try
        {
            readerWriterLockSlim.EnterUpgradeableReadLock();
            action();
        }
        finally
        {
            readerWriterLockSlim.ExitUpgradeableReadLock();
        }
    }
 
    public static T PerformUsingUpgradeableReadLock<T>(this ReaderWriterLockSlim readerWriterLockSlim, Func<T> action)
    {
        ArgumentValidator.AssertNotNull(readerWriterLockSlim, "readerWriterLockSlim");
        ArgumentValidator.AssertNotNull(action, "action");
        try
        {
            readerWriterLockSlim.EnterUpgradeableReadLock();
            return action();
        }
        finally
        {
            readerWriterLockSlim.ExitUpgradeableReadLock();
        }
    }
}

The ArgumentValidator class is present in my base library. Calls to its AssertNotNull can be replaced with a null check, and if null throw an ArgumentNullException. If you'd like the code for the ArgumentValidator etc., you can find it in the Core class library in the source available at http://calcium.codeplex.com/SourceControl/list/changesets

Conclusion

In this post we have seen how extension methods can be used to ensure that the ReaderWriterLockSlim class is correctly exited after a thread critical region. This avoids the problem of mismatched Enter and Exit calls, which can result in exceptions and indefinite blocking.

I hope you have enjoyed this post, and that you find the code and ideas presented within it useful.

Download code: ReaderWriterSlimLockExtensions.zip (1.14 mb)

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)

About the Author

Daniel Vaughan
Software Developer (Senior) Outcoder
Switzerland Switzerland
Member
Daniel Vaughan is a Microsoft MVP and cofounder of Outcoder, a Swiss software and consulting company dedicated to creating best-of-breed user experiences and leading-edge back-end solutions, using the Microsoft stack of technologies--in particular Silverlight, WPF, WinRT, and Windows Phone.
 
Daniel is the author of Windows Phone 7.5 Unleashed, the first comprehensive, start-to-finish developer's guide to Microsoft's Windows Phone 7.5.
 
Daniel is also the creator of a number of open-source projects, including Calcium SDK, and Clog.
 
Would you like Daniel to bring value to your organisation? Please contact

Daniel's Blog | MVP profile | Follow on Twitter
 
Windows Phone Experts

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralA small problem...mvpPaulo Zemek1 Dec '12 - 14:14 
I already read some of the posts, but I must say that doing the Enter (be it read, write or upgradeable) inside the try is generally worse than using the Enter then the try.
 
I know that ThreadAbortExceptions are very problematic, but the ReaderWriterLockSlim is not abort safe either, so putting the Enter inside or outside the try is not a real solution. You could make it completely abort safe by putting the Enter inside a finally block (but in that case you will never be able to abort immediatelly, so you should always use timeouts) and we can also enter in the old discussion that aborts should not be used and, if they are used when exiting AppDomains, then it is better to abort directly.
 
But the problem of putting the Enter inside the try is that:
1) If your thread is aborted while waiting to acquire the lock, it will still exit the lock (in many increasing the chances of corrupting it compared to using the Enter then the try, as that will corrupt only in situations were the Abort happens just after the enter and before the try).
2) As already stated, some other situations may cause an exception during the enter, as recursive problems, and in this case you will corrupt the lock again (it is better to have an exception and stop a single thread than to corrupt the lock).
GeneralRe: A small problem...memberDaniel Vaughan3 Dec '12 - 0:58 
Thanks for your input Paulo!
Looking back, I agree with you. Seems like we've come full circle. Smile | :)
 
Cheers,
Daniel
Daniel Vaughan
Twitter | Blog | Microsoft MVP | Projects: Calcium SDK, Clog | LinkedIn

QuestionProblem with code snippet formatting...memberGilly Barr25 Nov '12 - 23:31 
Maybe this happened since the new site design, but there's something really wrong with the code snippets in the article... :(
AnswerRe: Problem with code snippet formatting...memberDaniel Vaughan26 Nov '12 - 0:19 
Thanks for letting me know Gilly.
Daniel Vaughan
Twitter | Blog | Microsoft MVP | Projects: Calcium SDK, Clog | LinkedIn

QuestionSome issues that may have been overlookedmemberTravis Hall10 Jul '11 - 21:05 
Nice idea, but a couple of issues.
 
First, it seems to me that you can run into a problem if EnterXXXXLock throws an exception (which it can do if RecursionPolicy is set to NoRecursion - which is the default). In that case, the try block is exited without incrementing the lock count, and then the finally block will decrement the lock count. So, the EnterXXXXLock function should be outside of the try block. Unless there is something I've missed?
 
Secondly, the only time upgradeable read locks should be used would be when there is some chance that the lock will be upgraded to a write lock. That means that action() could alter the state of the ReaderWriterLockSlim object, and ExitUpgradeableReadLock may not be the correct function to exit the lock. So, this approach should not be used with upgradeable read locks. Again, unless I've overlooked something?
 
(Or does upgrading an upgradeable read lock to a write lock leave us with both an ungradeable read lock and a write lock that must both be exited? I'm not sure, and don't have time right now to test this. I definitely would before actually using upgradeable read locks, though.)
AnswerRe: Some issues that may have been overlookedmvpDaniel Vaughan10 Jul '11 - 21:26 
Hi Travis,
 
Travis Hall wrote:
First, it seems to me that you can run into a problem if EnterXXXXLock throws an exception (which it can do if RecursionPolicy is set to NoRecursion - which is the default). In that case, the try block is exited without incrementing the lock count, and then the finally block will decrement the lock count. So, the EnterXXXXLock function should be outside of the try block. Unless there is something I've missed?

 
If the enter call is place before the try and an exception is raised, such as a ThreadAbortException, then the exit call does not occur.
There is no difference to using ReaderWriterSlimLock in the traditional manner; it is not unique to this technique.
 
Travis Hall wrote:
Secondly, the only time upgradeable read locks should be used would be when there is some chance that the lock will be upgraded to a write lock. That means that action() could alter the state of the ReaderWriterLockSlim object, and ExitUpgradeableReadLock may not be the correct function to exit the lock. So, this approach should not be used with upgradeable read locks. Again, unless I've overlooked something?

 
It works in same manner. Calls to enter and exit must coincide.
 
Thanks for your message.
 
Cheers,
Daniel
Daniel Vaughan
Twitter | Blog | Microsoft MVP | Projects: Calcium SDK, Clog | LinkedIn

GeneralRe: Some issues that may have been overlookedmemberTravis Hall10 Jul '11 - 23:52 
Hi Daniel.
 
You're right about the inside/outside issue not being unique to the technique you described, and the basic technique has proved useful to me this afternoon. (I had been using the technique you discussed with somebody else, involving an IDisposable wrapper around a ReaderWriterLockSlim, and was getting odd behaviour as a result of how it interacted with multithreaded code. Your technique proved to avoid the multithreading issues. Though, I've now solved the problem both ways, as using your technique put my mind on the right track for solving it the other way too.)
 
But the "traditional" examples I've seen, including from MSDN, do keep the enter call outside the try.
 
If an exception - ThreadAbortException or otherwise, with the exception of LockRecursionException - occurred during an enter call, we'd have no way of knowing whether an exit call is appropriate. Is there a reason to favour calling exit when it could just as well be the wrong move? Unless there is a guarantee of atomicity on the enter call, in which case it should certainly go outside, as any exception thrown during the call would then be guaranteed to leave the state unaltered.
 
As for the second issue, I think you're telling me that the appropriate usage is as per my parenthesised paragraph. In which case, yeah, I just need to make sure I read up on the appropriate usage before using upgradeable read locks (which I haven't had a good reason to use yet).
 
Anyway, thanks for the discussion, and the article.
 
Travis
GeneralRe: Some issues that may have been overlookedmvpDaniel Vaughan11 Jul '11 - 1:39 
Hi Travis,
 
Thanks a lot for your thoughtful remarks.
 
I favoured placing the enter call in the try to avoid deadlocks at all costs. The downside is, as you say, not knowing if we should exit. Also, if the lock is not held then exiting will raise an exception. I will try and address issues like this in my next update.
 
Regarding the nesting of locks: yes, that's the way it works. There is, however, one exception: downgrading an upgradable read lock to a read-only lock; which can be done like so:
 
lockSlim.EnterUpgradeableReadLock();
try
{
    lockSlim.EnterReadLock();
    try
    {
        lockSlim.ExitUpgradeableReadLock();
        // In read mode.
    }
    finally
    {
        lockSlim.ExitReadLock();
    }
}
catch (Exception)
{
    if (lockSlim.IsUpgradeableReadLockHeld)
    {
        lockSlim.ExitUpgradeableReadLock();
    }
}
 
Cheers,
Daniel
Daniel Vaughan
Twitter | Blog | Microsoft MVP | Projects: Calcium SDK, Clog | LinkedIn

GeneralRe: Some issues that may have been overlooked [modified]memberTravis Hall13 Jul '11 - 16:43 
Hi Daniel.
 
The reason I was researching this stuff in the first place is because, in the code that was proving problematic, an early abort of a thread was resulting in a failed attempt to exit a lock (from another thread - specifically a garbage collection thread, because I was using an IDisposable wrapper around a ReaderWriterLockSlim). When I examined the state of the ReaderWriterLockSlim object just before the attempt to release the lock on the GC thread, I discovered that its lock counts were 0 - no locks held. And I know that the line of code I had written to call the exit function had not been hit.
 
Now, I'm using .NET Framework 4 here, and this has led me to wonder whether Microsoft improved the ReaderWriterLockSlim class to ensure that if a thread aborts early, all locks entered on that thread are released. In that case, deadlocks wouldn't be an issue and we'd be better off with our enters inside our try blocks.
 
If not, I guess one could wrap the exit in a try-catch-swallow (or try-catch-log, or try-catch-assert to make debugging easier) so that an exception thrown from the exit doesn't cause problems elsewhere.
 
Or possibly the behaviour I saw had to do with thread abortion during the creation of my wrapper object. I had thought I had guarded against that, but maybe my skills just weren't up to the job.
 
In any case, I've changed my approach for the current project. The only reason I was using a ReaderWriterLockSlim in the first place was because I needed to be able to test whether I could get a lock, returning immediately (from TryEnterXXXXLock) either way. This approach was how I had previously done it under .NET 3.5, and it turns out this isn't necessary any more. .NET 4 adds a new TryEnter method that handles this much better, so I can have both the deadlock safety and assurance I won't release a lock I don't have. Edit: Hmmph. It turns out that I could have done this under 3.5 as well. I must have missed this transitioning from .NET 2.
 
Anyway, just thought I'd point this out, just in case it proved to be interesting or useful to someone.
 
Travis

modified on Wednesday, July 13, 2011 10:55 PM

GeneralRe: Some issues that may have been overlookedmvpDaniel Vaughan13 Jul '11 - 22:36 
Hi Travis,
 
The IDisposable approach has a drawback in that if the 'enter' occurs during the instantiation of the disposable, and if an exception is raised during instantiation, then the 'exit' will not be called. This is analogous to the 'enter' call being outside the try catch block. The only way around that is to use a second call within the using block to perform the 'enter'. Not so elegant.
 
Travis Hall wrote:
Now, I'm using .NET Framework 4 here, and this has led me to wonder whether Microsoft improved the ReaderWriterLockSlim class to ensure that if a thread aborts early, all locks entered on that thread are released. In that case, deadlocks wouldn't be an issue and we'd be better off with our enters inside our try blocks.

If this is the case then it would certainly change things. I'm presuming this would entail some specialized CLR intervention for thread aborts. If you discover anything more around this please let me know.
 
Travis Hall wrote:
If not, I guess one could wrap the exit in a try-catch-swallow (or try-catch-log, or try-catch-assert to make debugging easier) so that an exception thrown from the exit doesn't cause problems elsewhere.

I reckon this is probably a good approach for now.
 
Thanks a lot for your message.
 
Cheers,
Daniel
Daniel Vaughan
Twitter | Blog | Microsoft MVP | Projects: Calcium SDK, Clog | LinkedIn

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 26 Nov 2012
Article Copyright 2010 by Daniel Vaughan
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid