Some projects suffer from a bad usage of the upgradeable-read-lock. If all the situations involve getting an upgradeable-read-lock to maybe upgrade to a write lock, without ever using simple read locks, there is unnecessary contention, which is worse than using full locks with the lock keyword.
In C#, we have the
lock keyword, which can lock any object in an exclusive way. That is, once one thread holds the lock, no other thread will be able to acquire the lock until the first thread releases it.
Such a lock is called exclusive (or sometimes a "full" lock) as there is no way for two threads to hold the lock at the same time.
An alternative to a full lock is a reader-writer lock. With a reader-writer lock, many readers can acquire the lock at the same time but a writer lock is actually an exclusive lock. If a thread acquires a write-lock, no other thread can acquire either a read or a write lock.
.NET had first a class named
ReaderWriterLock but it is somewhat obsolete and a new class, named
ReaderWriterLockSlim, is the recommended class for when we want reader-writer locks. Yet, such a class is many times used incorrectly, effectively becoming less efficient than always using a full lock. This article is all about understanding such a problem and fixing it.
The Traits of ReaderWriterLockSlim
Although the class has "Slim" in its name, the
ReaderWriterLockSlim isn't really that slim, and it is recommended that you use its
Dispose() method to release its resources when you are done with it.
Anyway, one of the interesting traits is that the class provides three locking methods, not just two.
Read-locks and write-locks work as expected from a simple reader-writer lock, but there's a third lock, named upgradeable-read-lock.
An upgradeable-read-lock is recommended for cases where the creation/loading of a resource might take a long time, so we don't want two or more threads doing the same work in parallel, but we also don't want to stop readers from reading the object as they might be accessing different data that's already loaded (like in a caching scenario).
So, the practice can be described as:
- Acquire the upgradeable-read-lock
- Check if the data is already there and, if it is, return it. If not...
- Generate/load the data
- Acquire the write lock
- Store the data
- Return the data
As I see, there are two main problems with the pattern I just described:
- I didn't talk about releasing the locks. When we use the
lock keyword, the lock is released automatically when the block of code ends. With a
ReaderWriterLockSlim, we need to explicitly call
Exit for the appropriate type of lock that was acquired.
- This pattern is just incomplete if we don't have methods that are just readers. If all the methods check for the presence of data while holding an upgradeable-read-lock, the code is just doing a slower exclusive lock, as the upgradeable-read-lock only allows plain read-locks to be obtained in parallel, not other upgradeable-read-locks.
Solving the Problems
For the first problem, the fact that we need to manually call the right
Exit manually, we can use extension methods and return
structs that implement
IDisposable to do the exit.
For example, instead of doing:
We could just do:
To achieve that, we need code like this:
public readonly struct UpgradeableLockDisposer:
private readonly ReaderWriterLockSlim _rwLock;
public UpgradeableLockDisposer(ReaderWriterLockSlim rwLock)
_rwLock = rwLock;
public void Dispose()
public static UpgradeableLockDisposer UpgradeableLock(this ReaderWriterLockSlim rwLock)
return new UpgradeableLockDisposer(rwLock);
struct was made
public and the
UpgradeableLock is returning it by its type, instead of returning it just as an
IDisposable, to avoid any boxing or any memory allocation. Assuming the compiler is doing the right optimizations, the performance of this helper method should be the same as the one using
For the second problem, we need a double-checked triple lock pattern. That is, we need to do something like:
var data = TryGetData();
if (data != null)
var data = TryGetData();
if (data != null)
data = CallMethodToGenerateTheData();
Notice that we use the 3 lock kinds.
First, we use just a read-lock. This means many threads can access the data at the same time. Assuming the data is not there, we need to release the read lock to then acquire the other lock.
Second, we acquire the upgradeable-read-lock and need to check for the data again, as it might have been added while we acquired this second lock. Assuming the data is not there, we can load/generate the data, knowing that no other thread will be doing that, as no two threads can have an upgradeable-lock at the same time.
Finally, when we have the data, we need to get the write-lock, which means all threads that have a read-lock (if any) need to release their locks, to then write the data, before returning.
Thanks to the
using clauses, we don't need to worry about any
Exit calls, but they would be done appropriately when leaving the
A New Problem
A triple lock approach might sound excessive. It will kill the performance, right?
Well... it might. As I said before, the
ReaderWriterLockSlim is not that slim and it is possible that acquiring a full lock and reading a value might be quicker than just acquiring the read-lock. That's one of the reasons that made me write alternative locking classes, which I presented in the article Managed Thread Synchronization.
But still talking about the
ReaderWriterLockSlim, trying to avoid just the read-lock is a no-no. If just upgradeable-locks are used, it is similar to doing full locks all the time... but slower.
Doing just read-locks and write-locks, without ever using the upgradeable lock might be a valid approach if loading the data is actually quick. But, if that's the case, maybe using a
ConcurrentDictionary (assuming we have keyed data) or
Lazy (assuming a single data) would be better.
In any case, if you are using the
ReaderWriterLockSlim, you must be aware that the read-lock must be used. Using just the upgradeable-lock and the write-lock is just wrong.
Also, even if the triple lock pattern seems too much, after the data is loaded, the code would be just using read-locks (possibly in parallel) to read the data and would not use the upgradeable and write locks again.
The Download Sample
The download sample has a
ReaderWriterLockSlimExtensions class with the three lock types being returned as
IDisposable so you can use the
using clause to automate the release of the locks.
If you like, you can just add its file to any project. Feel free to put the class into a namespace if you prefer.
- 3rd May, 2023: Initial version