RSS 2.0

Personal Info:

Joe Send mail to the author(s) is the lead developer and architect for Parallel Extensions to .NET, tinkers with type systems, and is an author and speaker.

Blogroll:
Other
News
 C|Net
 Kuro5hin
 The Register
Technology
 <?xmlhack?>
 Daily WTF
 DevX
 Hacknot
 Java Today
 Microsoft Top 10 Downloads
 MSDN
 MSDN: "Longhorn"
 MSDN: XML Developer Center
 Slashdot
 Techdirt
 theserverside.com
 W3C
 Web Pages That Suck
 XML Cover Pages
 XML Journal
 xml.com
Technology Blogs
 Aaron Skonnard [PluralSight]
 Adam Bosworth [Google]
 Andy Rich [MS/C++]
 Arpan Desai [MS/XML]
 BCL Team [MS]
 Bill Clementson [Lisp]
 Bill de hÓra
 Bruce Eckel [J]
 Bruce Tate [J]
 Casey Chestnut
 Cedric Beust [Google]
 Chris Anderson [MS/Avalon]
 Chris Lyon [MS]
 Christian Weyer
 Clemens Vasters [newtelligence]
 Craig Andera [PluralSight]
 Dan Sugalski [Parrot]
 Daniel Cazzulino
 Dave Chappel
 Dave Roberts [Lisp]
 Dave Thomas [PragProg]
 Dave Winer
 Dion Almaer [J]
 Don Demsak
 Doug Purdy [MS/Indigo]
 Drew Marsh
 Eric Gunnerson [MS]
 Eric Rudder [MS]
 Eric Sink
 Fritz Onion [PluaralSight]
 Gavin King [J/Hibernate]
 Grady Booch [IBM]
 Hervey Wilson [MS/Indigo]
 Hillel Cooperman [MS/Shell]
 Howard Lewis Ship [J/Apache]
 Ingo Rammer [PluralSight]
 James Gosling [J/Sun]
 James Strachan [J/Groovy]
 Jason Matusow [MS/OSS]
 Jeffrey Schlimmer [MS/Indigo]
 Joe Beda [Google]
 Joel Spoelsky
 Jon Udell
 Josh Ledgard [MS/Evang]
 Joshua Allen [MS]
 Lambda
 Larry Osterman [MS]
 Maoni Stephens [MS/CLR]
 Mark Fussell [MS/XML]
 Martin Fowler
 Martin Gudgin [MS/Indigo]
 Me
 Michael Howard [MS]
 Miguel de Icaza [Mono]
 Mike Clark
 Omri Gazitt [MS/Indigo]
 Pat Helland [MS/PAG]
 Pinku Surana
 Raymond Chen [MS]
 Rich Lander [MS/CLR]
 Rob Howard
 Rob Relyea [MS/Avalon]
 Robert Cringely
 S. Somasegar [MS/DevDiv]
 Sam Gentile
 Scoble [MS/Evang]
 Scott Guthrie [MS/WebNet]
 Scott Hanselman
 Sean McGrath [J]
 Simon Fell
 Stanley Lippman [MS/C++]
 Steve Maine
 Steve Swartz [MS/Indigo]
 Steve Vinoski
 Steven Clarke [MS/Usability]
 Stuart Halloway
 Ted Leung
 Ted Neward [DM]
 Tim Bray [Sun]
 Tim Ewald [Mindreef]
 Tim O'Reilly
 Werner Vogels [Amazon]
 Wintellect
 Yasser Shohoud [MS/Indigo]
Top 20
 Brad Abrams [MS/CLR]
 Chris Brumme [MS/CLR]
 Chris Sells [MS/Ultra]
 Cyrus Najmabadi [MS/C#]
 Dominic Cooney [MS/XAF]
 Don Box [MS/Ultra]
 Don Syme [MS/R]
 Guido van Rossum [Python]
 Herb Sutter [MS/C++]
 Ian Griffiths
 Jason Zander [MS/CLR]
 Jim Hugunin [MS/CLR]
 Joel Pobar [MS/CLR]
 Krzysztof Cwalina [MS/CLR]
 Patrick Logan
 Paul Graham
 Rico Mariani [MS/CLR]
 Rory Blyth [MS/DN]
 Sam Ruby
 Wesner Moise
VC/Business Blogs
 Ed Sim
 Fred Wilson
 Jonathan Schwartz [J/Sun]
 Lawrence Lessig [Stanford]
 Mark Cuban
 Michael Hyatt
 Pierre Omidyar
 Ross Mayfield
 VentureBlog
 Weekly Read
Wine, Food & Tea
 The Silk Road of Wine
 Vinography: a wine blog
 Wine Whys

Disclaimer:
The content of this site are my own personal opinions and do not represent my employer's view in anyway.

© 2009, Joe Duffy

 
 Wednesday, February 07, 2007

In Orcas, we offer a new reader/writer lock: System.Threading.ReaderWriterLockSlim.

Motivation for a new lock

The primary reason for creating this type was that we wanted to provide an official reader/writer lock for the .NET Framework that people could actually rely on for performance-critical code.  It was no secret that the current ReaderWriterLock type was such a pig, costing somewhere around 6X the cost of a monitor acquisition for uncontended write lock acquires, that most people avoided it entirely.  Jeff Richter wrote an entire MSDN article about this, and Vance Morrison showed how to build your own on his weblog.  It was really too bad customers couldn't depend on the class in the Framework, and to be honest most devs really shouldn't be in the business of writing and maintaining their own reader/writer lock.

Second, we had a large number of qualms with the existing lock’s design.  It had funny recursion semantics (and is in fact broken in a few thread reentrancy cases we know about), and has a dangerous non-atomic upgrade method.  Did you know that you actually need to check the WriterSeqNum before and after a call to our ReaderWriterLock’s UpgradeToWriteLock method to ensure it didn’t change during the upgrade?  You do.  The code actually releases the reader lock before upgrading to the write lock, which allows other threads to sneak in, acquire the lock in between, and possibly change state that was read during the decision to upgrade.  The reason?  If we upgraded and then released the read lock, two threads trying to simultaneously upgrade would deadlock one another.  All of these problems represent very fundamental flaws in the existing type’s design.

So we decided to build a new one that solves all of these problems.  To be honest, I would have liked to fix the current one, but the existing API and compatibility responsibilities make that just about impossible.  We considered obsoleting the existing one, but as I note at the end of this article, there are still reasons to prefer the old lock.

Three modes: read, write, and upgradeable-read

The new ReaderWriterLockSlim supports three lock modes: Read, Write, and UpgradeableRead, and has the methods EnterReadLock, EnterWriteLock, EnterUpgradeableReadLock, and corresponding TryEnterXXLock and ExitXXLock methods, that do what you’d expect.  Read and Write are easy and should be familiar: Read is a typical shared lock mode, where any number of threads can acquire the lock in the Read mode simultaneously, and Write is a mutual exclusion mode, where no other threads are permitted to simultaneously hold the lock in any mode.  The UpgradeableReadLock will probably be new to most people, though it’s a concept that’s well known to database folks, and is the magic that allows us to fix the upgrade problem I mentioned earlier.  We’ll look at it more closely in a moment.

The performance of the new lock is roughly equal to that of Monitor.  When I say “roughly”, I mean that it’s within a factor of 2X in just about all cases.  And the new lock favors letting threads acquire the lock in Write mode over Read or UpgradeableRead, since writers tend to be less frequent than readers, generally leading to better scalability.  We’d originally considered providing a set of contention management options to choose from, but decided in the end to ship a simpler design that works well for most cases.

Upgrading

Let’s look at upgrades more closely now.  The UpgradeableRead mode allows you to safely upgrade from Read to Write mode.  Remember I mentioned earlier that our old lock breaks atomicity in order to provide deadlock-free upgrade capabilities (which is bad, particularly since most people don't realize it).  The new lock neither breaks atomicity nor causes deadlocks.  We acheive this by allowing only one thread to be in the UpgradeableRead mode at once, though there may be any number of other threads in Read mode while there’s one UpgradeableRead owner.

Once the lock is held in the UpgradeableRead mode, a thread can then read state to determine whether to downgrade to Read or upgrade to Write.  Note that this decision should ideally be made as fast as possible: holding the UpgradeableRead lock forces any new Read acquisitions to wait, though existing Read holders are still permitted to remain active.  (Sadly the CLR team seems to have removed two methods, DowngradeToRead and UpgradeToWrite, that I had originally designed for this purpose.  I admit what follows isn’t the most obvious way to do it.)   To downgrade, you simply call EnterReadLock followed by ExitUpgradeableReadLock: this permits other Read and UpgradeableRead acquisitions to finish that were previously held up by the fact that there was an UpgradeableRead lock held.  To upgrade, you similarly call EnterWriteLock: this may actually have to wait until there are no longer any threads that still hold the lock in Read mode.  There’s no real reason to also exit the UpgradeableReadLock at this point unlike the downgrade case, though in some cases it makes your code more uniform.  E.g.:

ReaderWriterLockSlim rwl = …;

bool upgraded = true;
rwl.EnterUpgradeableReadLock();
try {
    if (… read some state to decide whether to upgrade …) {
        rwl.EnterWriteLock();
        try {
            … write to state …
        } finally {
            rwl.ExitWriteLock();
        }
    } else {
        rwl.EnterReadLock();
        rwl.ExitUpgradeableReadLock();
        upgraded = false;
        try {
            … read from state …
        } finally {
            rwl.ExitReadLock();
        }
    }
} finally {
    if (upgraded)
        rwl.ExitUpgradeableReadLock();
}

Recursive acquires

Another nice feature with the new lock is how it treats recursion.  By default, all recursive acquires, aside from the upgrade and downgrade cases already mentioned, is disallowed.  This means you can’t call EnterReadLock twice on the same thread without first exiting the lock, for example, and similarly with the other modes.  If you try, you get a LockRecursionException thrown at you.  You can, however, turn recursion on at construction time: pass the enum value LockRecursionPolicy.SupportsRecursion to your lock’s constructor, and voila, recursion will be permitted.  The chosen policy for a given lock is subsequently accessible from its RecursionPolicy property.

There’s one special case that is never permitted, regardless of the lock recursion policy: acquiring a Write lock when a Read lock is held.  We considered enabling this, or at least giving a new enum value for it, but decided to hold off for now: if it turns out customers need it, we can always add it later.  But it’s dangerous and leads to the same Read-to-Write upgrade deadlocks that the old lock was prone to, and so we didn’t want to lead developers down a path fraught with danger.  If you need this kind of recursion, it’s a “simple” matter of changing your design to hoist a call to either EnterWriteLock or EnterUpgradeableReadLock (and the corresponding Exit method) to the outermost scope in which the lock is acquired.

There are corresponding properties IsReadLockHeld, IsWriteLockHeld, and IsUpgradeableReadLockHeld, to determine whether the current thread holds the lock in the specified mode.  You can also query the WaitingReadCount, WaitingWriteCount, and WaitingUpgradeCount properties to see how many threads are waiting to acquire the lock in the specific mode, and CurrentReadCount to see how many concurrent readers there are.  The RecursiveReadCount, RecursiveWriteCount, and RecursiveUpgradeCount properties tell you how many recursive acquires the current thread has made for the specific mode.

Some limitations: reliability

Lastly, I mentioned there are some caveats around where this lock’s use is appropriate.  Well, there’s one, really: it’s not hardened to be reliable.  This means a few things.

First, unlike the existing ReaderWriterLock, the ReaderWriterLockSlim type does not cooperate with CLR hosts through the hosting APIs.  This means a host will not be given a chance to override various lock behaviors, including performing deadlock detection (as SQL Server does).  Thus, you really ought not to use this lock if your code will be run inside SQL Server.

Next, the lock is not robust to asynchronous exceptions such as thread aborts and out of memory conditions.  If one of these occurs while in the middle of one of the lock’s methods, the lock state can be corrupt, causing subsequent deadlocks, unhandled exceptions, and (sadly) due to the use of spin locks internally, a pegged 100% CPU.  So if you’re going to be running your code in an environment that regularly uses thread aborts or attempts to survive hard OOMs, you’re not going to be happy with this lock.  Unfortunately the lock doesn’t even mark critical regions appropriately, so hosts that do make use of thread aborts won’t know that the thread abort could possibly put the AppDomain at risk: many hosts would prefer to wait, or immediately escalate to an AppDomain unload, if an individual thread abort is necessary while the thread is in a critical region.  But in the case of ReaderWriterLockSlim, a host has no idea if a thread holds the lock because the implementation doesn’t call Begin- and EndCriticalRegion.  And the kind of problems I mentioned in the previous post are always an issue with ReaderWriterLockSlim, because we don't necessarily guarantee that there will be no instructions in the JIT-generated code between the acquisition and entrance to the following try block.

Summary

In summary, the new ReaderWriterLockSlim lock eliminates all of the major adoption blockers that plagued the old ReaderWriterLock.  It performs much better, has deadlock-free and atomicity-preserving upgrades, and leads developers to program cleaner designs free of lock recursion.  There are some downsides to the new lock, however, that may cause programmers writing hosted or low-level reliability-sensitive code to wait to adopt it.  Don’t get me wrong, most people really don’t need to worry about these topics, so I apologize if my words of warning have scared you off: but those that do really need to be told about the state of affairs.  Thankfully, I’m confident that many of these issues will be fixed in subsequent releases.  And for most developers out there, the new ReaderWriterLockSlim is perfect for the job.

2/7/2007 12:55:14 PM (Pacific Standard Time, UTC-08:00)
Is this based on the Win32 SRWLock synchronization primitive in Vista/Longhorn later so it only will be supported by these environments?
2/7/2007 1:18:07 PM (Pacific Standard Time, UTC-08:00)
Does entering / exiting the lock provide memory barriers in the same way that using a Monitor does?
2/7/2007 1:27:47 PM (Pacific Standard Time, UTC-08:00)
---8<---
rwl.EnterReadLock();
rwl.ExitUpgradeableReadLock();
--->8---

I think your original design had it right - this is more awkward. The new design has three distinct acquired states rather than two in the old RWL, so having explicit labels for the valid transitions between the acquired states would make it more conceptually coherent, IMHO.

A pity.
2/7/2007 7:09:51 PM (Pacific Standard Time, UTC-08:00)
Michel: This isn't based on the new SRWLock in Vista. So it will work on any OS that Orcas runs on.

Chris: Yes, there is a barrier for entrance and exit. If there wasn't, the lock would be subject to causality problems, even on X86.

Barry: I totally agree. I have since left the team, so I don't know why they cut it. The reality is far fewer people will likely use UpgradeableReadLocks as a result. At least it can always be added in the future, based on customer feedback.
2/8/2007 3:54:24 PM (Pacific Standard Time, UTC-08:00)
Hi

Do you know why it is 5x slower even if there is no contention ?


Thanks
nativecpp
2/8/2007 4:12:26 PM (Pacific Standard Time, UTC-08:00)
BTW, can you also explain the question from Chris and your answer as well?


Thanks
nativecpp
2/9/2007 2:00:22 PM (Pacific Standard Time, UTC-08:00)
Is ReaderWriterLockSlim a disposable type? I hope not. I prefer lock types that I don't have to call Dispose on so they can be used as member fields in non-disposable classes.
2/9/2007 8:32:05 PM (Pacific Standard Time, UTC-08:00)
NativeCpp: Most of the cost of the old RWL is attributed to managed-to-native transitions. Monitor, though written in native too, enjoys a lot of JIT helper support. This isn't the only cost (else we could have probably added intrinsic support for RWL too). The implementation is VERY complicated, and has to support the strange recursion behavior and acts specially for COM interop. Just to be clear: the OLD RWL is ~5x the cost, not the new one.

Bill: yes, it is a disposable type. I personally prefer this: since all locks with true waiting require event objects (at least those that don't use Win32 keyed events), you really want to have the option of disposing of it. Monitor hides this fact which can be annoying. Using it is of course not required, of course-- the event has a SafeHandle with a finalizer to take care of cases where you forget. Note that the event is lazily allocated, which means sometimes rwl.Dispose() is a no-op.

--joe
2/12/2007 5:45:52 AM (Pacific Standard Time, UTC-08:00)
Thanks for the reply. I just feel "contractually obligated" to call dispose when I'm using a disposable field. Unfortunately, it's a contract-breaking change to add IDisposable to an existing class. So now I can't transparently change any of my existing types that use ReaderWriterLock over to use ReaderWriterLockSlim without violating one of these rules. I either have to skip calling dispose on the slim lock, or I have to implement IDisposable and "break" all of my existing clients.

But it is a VERY strong argument that by making ReaderWriterLockSlim disposable that it gives everyone the opportunity to deterministically clean up its resources, whereas if it wasn't disposable (like Monitor and ReaderWriterLock) then you're forced to wait on the GC. I'll probably just switch my classes over to using ReaderWriterLockSlim and not call dispose on it. It shouldn't be any worse off than today's ReaderWriterLock if it is using events internally that are only cleaned up by the GC (as Vance Morrison just told me at http://blogs.msdn.com/vancem/archive/2006/03/29/564854.aspx?CommentPosted=true#commentmessage).
2/12/2007 8:37:16 AM (Pacific Standard Time, UTC-08:00)
Hi Joe,

Thanks for the reply. Could you explain briefly about the memory barriers or at least supply me a link to study more about this subject?
nativecpp
7/4/2007 4:34:34 AM (Pacific Daylight Time, UTC-07:00)
Don’t programmers get satisfaction when their software is used it for *anything*? Does it really matter if it’s free, commercial, open or proprietary? Aren’t we taking the meritocracy just a bit too far? I think the GPL is beginning to slow down and hamper open source adoption for exactly the reason it was created. It’s time to change.
7/11/2007 8:31:56 AM (Pacific Daylight Time, UTC-07:00)
Where I am ?
http://buschgardens.givout.com
http://savage.royalrealestateagency.com
http://modelkit.traficexperts.com
http://penbig.searchtrafficwizard.com
boltly
Name
E-mail
Home page

Comment (HTML not allowed)  

Enter the code shown (prevents robots):

 

Recent Entries:

Search:

Browse by Date:
<January 2009>
SunMonTueWedThuFriSat
28293031123
45678910
11121314151617
18192021222324
25262728293031
1234567

Browse by Category:

Notables:

Currently Up To:

Reading...

Listening...

Watching...