I'm very familiar with ReaderWriterLockSlim
but tried my hand at implementing EnterUpgradeableReadLock()
recently in a class... Soon after I realized that this is almost certainly a guaranteed deadlock when 2 or more threads run the code:
Thread A --> enter upgradeable read lock
Thread B --> enter upgradeable read lock
Thread A --> tries to enter write lock, blocks for B to leave read
Thread B --> tries to enter write lock, blocks for A to leave read
Thread A --> waiting for B to exit read lock
Thread B --> waiting for A to exit read lock
What am I missing here?
EDIT
Added code example of my scenario. The Run()
method would be called by 2 or more threads concurrently.
public class Deadlocker
{
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
public void Run()
{
_lock.EnterUpgradeableReadLock();
try
{
_lock.EnterWriteLock();
try
{
// Do something
}
finally
{
_lock.ExitWriteLock();
}
}
finally
{
_lock.ExitUpgradeableReadLock();
}
}
}
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With