I\'m very familiar with ReaderWriterLockSlim
but tried my hand at implementing EnterUpgradeableReadLock()
recently in a class... Soon after I reali
A long time after the OP, but I don't agree with the currently accepted answer.
The statement Thread B --> enter upgradeable read lock
is incorrect. From the docs
Only one thread can be in upgradeable mode at any time
And in response to your comments: it is intended for a very different usage to a Read-Write pattern.
TL;DR. Upgradeable mode is useful:
Or, in pseudocode, where this:
// no other writers or upgradeables allowed in here => no race conditions
EnterUpgradeableLock();
if (isWriteRequired()) { EnterWriteLock(); DoWrite(); ExitWriteLock(); }
ExitUpgradeableLock();
gives "better performance" ÷ than this:
EnterWriteLock(); if (isWriteRequired()) { DoWrite(); } ExitWriteLock();
It should be used with care if the exclusive lock sections take a very long time due to it's use of SpinLock.
The Upgradeable lock is surprisingly similar to a SQL server SIX lock (Shared with Intent to go eXclusive) †.
Without the existence of an Intent lock, you must perform the "should I make this change" check inside an eXclusive lock, which can hurt concurrency.
If the Upgradeable lock was shareable with other Upgradeable locks it would be possible to have a race condition with other Upgradeable lock owners. You would therefore require yet another check once inside the Write lock, removing the benefits of doing the check without preventing other reads in the first place.
If we view all lock wait/entry/exit events as sequential, and the work inside a lock as parallel, then we can write a scenario in "Marble" form (e
enter; w
wait; x
exit; cr
check resource; mr
mutate resource; R
Shared/Read; U
Intent/Upgradeable; W
eXclusive/Write):
1--eU--cr--wW----eW--mr--xWxU--------------
2------eR----xR----------------eR--xR------
3--------eR----xR--------------------------
4----wU----------------------eU--cr--xU----
In words: T1 enters the Upgradeable/Intent lock. T4 waits for the Upgradeable/Intent lock. T2 and T3 enter read locks. T1 meanwhile checks the resource, wins the race and waits for an eXclusive/Write lock. T2&T3 exit their locks. T1 enters the eXclusive/Write lock and makes the change. T4 enters the Upgradeable/Intent lock, doesn't need to make it's change and exits, without blocking T2 which does another read in the meantime.
The Upgradeable lock is:
Upgradeable is not required if one of the following apply (including but not limited to):
writelock-check-nowrite-exit
are approximately zero (the write-condition check is super fast) - i.e. an Upgradeable construct doesn't help Reader throughput;The probability of having a writer which Writes once in a Write lock is ~1 because either:
ReadLock-Check-WriteLock-DoubleCheck
is so fast it only causes race losers once per trillion writes; It is also not required if a lock(...){...}
is more appropriate, i.e.:
÷ Where "performance" is up to you to define
† If you view the lock object as the table, and the protected resources as the resources lower in the hierarchy, this analogy approximately holds
‡ The initial check in a Read lock would be optional, the check within the Upgradeable lock is mandatory, therefore it can be used in a single or double check pattern.
You have an error in your example
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
it should be
private static readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
Now in your code everytime a class is instantianed it is creating new instance of ReaderWriterLockSlim which is unable to lock anything because every single thread has it's own instance of it. Making it static will force all threads to use one instance which will work as it should
I recommend avoiding EnterUpgradeableReadLock(). Just use EnterWriteLock() instead. I know that seems inefficient, the upgradeable read lock is almost as bad as a write lock anyway.