Quite often, instances of a type are thread-safe for concurrent read operations, but not for concurrent updates (nor for a concurrent read and update). This can also be true with resources such as a file. Although protecting instances of such types with a simple exclusive lock for all modes of access usually does the trick, it can unreasonably restrict concurrency if there are many readers and just occasional updates. An example of where this could occur is in a business application server, where commonly used data is cached for fast retrieval in static fields. The ReaderWriterLockSlim
class is designed to provide maximum-availability locking in just this scenario.
With both classes, there are two basic kinds of lock — a read lock and a write lock:
- A write lock is universally exclusive.
- A read lock is compatible with other read locks.
So, a thread holding a write lock blocks all other threads trying to obtain a reador write lock (and vice versa). But if no thread holds a write lock, any number of threads may concurrently obtain a read lock.
ReaderWriterLockSlim
defines the following methods for obtaining and releasing read/write locks:
public void EnterReadLock(); public void ExitReadLock(); public void EnterWriteLock(); public void ExitWriteLock();
Additionally, there are “Try” versions of all Enter
XXX methods that accept timeout arguments in the style ofMonitor.TryEnter
(timeouts can occur quite easily if the resource is heavily contended). ReaderWriterLock
provides similar methods, named Acquire
XXX and Release
XXX. These throw an ApplicationException
if a timeout occurs, rather than returning false
.
The following program demonstrates ReaderWriterLockSlim
. Three threads continually enumerate a list, while two further threads append a random number to the list every second. A read lock protects the list readers, and a write lock protects the list writers:
}
Here’s the result:
Thread B added 61 Thread A added 83 Thread B added 55 Thread A added 33 ...
ReaderWriterLockSlim
allows more concurrent Read
activity than a simple lock. We can illustrate this by inserting the following line in the Write
method, at the start of the while
loop:
Console.WriteLine (_rw.CurrentReadCount + " concurrent readers");
This nearly always prints “3 concurrent readers” (the Read
methods spend most of their time inside the foreach
loops). As well as CurrentReadCount
, ReaderWriterLockSlim
provides the following properties for monitoring locks:
public bool IsReadLockHeld { get; }
public bool IsUpgradeableReadLockHeld { get; }
public bool IsWriteLockHeld { get; }
public int WaitingReadCount { get; }
public int WaitingUpgradeCount { get; }
public int WaitingWriteCount { get; }
public int RecursiveReadCount { get; }
public int RecursiveUpgradeCount { get; }
public int RecursiveWriteCount { get; }
Upgradeable Locks and Recursion
Sometimes it’s useful to swap a read lock for a write lock in a single atomic operation. For instance, suppose you want to add an item to a list only if the item wasn’t already present. Ideally, you’d want to minimize the time spent holding the (exclusive) write lock, so you might proceed as follows:
- Obtain a read lock.
- Test if the item is already present in the list, and if so, release the lock and
return
. - Release the read lock.
- Obtain a write lock.
- Add the item.
The problem is that another thread could sneak in and modify the list (e.g., adding the same item) between steps 3 and 4. ReaderWriterLockSlim
addresses this through a third kind of lock called an upgradeable lock. An upgradeable lock is like a read lock except that it can later be promoted to a write lock in an atomic operation. Here’s how you use it:
- Call
EnterUpgradeableReadLock
. - Perform read-based activities (e.g., test whether the item is already present in the list).
- Call
EnterWriteLock
(this converts the upgradeable lock to a write lock). - Perform write-based activities (e.g., add the item to the list).
- Call
ExitWriteLock
(this converts the write lock back to an upgradeable lock). - Perform any other read-based activities.
- Call
ExitUpgradeableReadLock
.
From the caller’s perspective, it’s rather like nested or recursive locking. Functionally, though, in step 3,ReaderWriterLockSlim
releases your read lock and obtains a fresh write lock, atomically.
There’s another important difference between upgradeable locks and read locks. While an upgradeable lock can coexist with any number of read locks, only one upgradeable lock can itself be taken out at a time. This prevents conversion deadlocks by serializing competing conversions — just as update locks do in SQL Server:
SQL Server | ReaderWriterLockSlim |
---|---|
Share lock | Read lock |
Exclusive lock | Write lock |
Update lock | Upgradeable lock |
We can demonstrate an upgradeable lock by changing the Write
method in the preceding example such that it adds a number to list only if not already present:
}
Lock recursion
Ordinarily, nested or recursive locking is prohibited with ReaderWriterLockSlim
. Hence, the following throws an exception:
var rw = new ReaderWriterLockSlim();
rw.EnterReadLock();
rw.EnterReadLock();
rw.ExitReadLock();
rw.ExitReadLock();
It runs without error, however, if you construct ReaderWriterLockSlim
as follows:
var rw = new ReaderWriterLockSlim (LockRecursionPolicy.SupportsRecursion);
This ensures that recursive locking can happen only if you plan for it. Recursive locking can create undesired complexity because it’s possible to acquire more than one kind of lock:
rw.EnterWriteLock();
rw.EnterReadLock();
Console.WriteLine (rw.IsReadLockHeld); // True
Console.WriteLine (rw.IsWriteLockHeld); // True
rw.ExitReadLock();
rw.ExitWriteLock();
The basic rule is that once you’ve acquired a lock, subsequent recursive locks can be less, but not greater, on the following scale:
Read Lock, Upgradeable Lock, Write Lock
A request to promote an upgradeable lock to a write lock, however, is always legal.