Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit ebf916f

Browse files
kouvelsafern
authored andcommitted
Fix new deadlock possibility in ReaderWriterLockSlim from a recent change (#14337)
Fix new deadlock possibility in ReaderWriterLockSlim from a recent change Fixed the following scenario that would lead to deadlock (rare, but frequently reproducible if code is changed): - Thread T0 signals the write waiter event or the upgradeable read waiter event to wake a waiter - There are no threads waiting on the event, but T1 is in WaitOnEvent() after exiting the spin lock and before actually waiting on the event (that is, it's recorded that there is one waiter for the event). It remains in this region for a while, in the repro case it typically gets context-switched out. - T2 acquires the RW lock in some fashion that blocks T0 or T3 from acquiring the RW lock - T0 or T3 fails to acquire the RW lock enough times for it to enter WaitOnEvent for the same event as T1 - T0 or T3 resets the event - T2 releases the RW lock and does not wake a waiter because the reset at the previous step lost a signal but _waiterStates was not updated to reflect that - T1 and other threads begin waiting on the event, but there's no longer any thread that would wake them Signed-off-by: dotnet-bot-corefx-mirror <dotnet-bot@microsoft.com>
1 parent 6cd4651 commit ebf916f

File tree

1 file changed

+48
-25
lines changed

1 file changed

+48
-25
lines changed

src/Common/src/CoreLib/System/Threading/ReaderWriterLockSlim.cs

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public class ReaderWriterLockSlim : IDisposable
5555
{
5656
private static readonly int ProcessorCount = Environment.ProcessorCount;
5757

58-
//Specifying if locked can be reacquired recursively.
58+
//Specifying if the lock can be reacquired recursively.
5959
private readonly bool _fIsReentrant;
6060

6161
// Lock specification for _spinLock: This lock protects exactly the local fields associated with this
@@ -967,7 +967,51 @@ private bool WaitOnEvent(
967967
Debug.Assert(_spinLock.IsHeld);
968968
#endif
969969

970+
WaiterStates waiterSignaledState = WaiterStates.None;
971+
EnterSpinLockReason enterMyLockReason;
972+
switch (enterLockType)
973+
{
974+
case EnterLockType.UpgradeableRead:
975+
waiterSignaledState = WaiterStates.UpgradeableReadWaiterSignaled;
976+
goto case EnterLockType.Read;
977+
978+
case EnterLockType.Read:
979+
enterMyLockReason = EnterSpinLockReason.EnterAnyRead;
980+
break;
981+
982+
case EnterLockType.Write:
983+
waiterSignaledState = WaiterStates.WriteWaiterSignaled;
984+
enterMyLockReason = EnterSpinLockReason.EnterWrite;
985+
break;
986+
987+
default:
988+
Debug.Assert(enterLockType == EnterLockType.UpgradeToWrite);
989+
enterMyLockReason = EnterSpinLockReason.UpgradeToWrite;
990+
break;
991+
}
992+
993+
// It was not possible to acquire the RW lock because some other thread was holding some type of lock. The other
994+
// thread, when it releases its lock, will wake appropriate waiters. Along with resetting the wait event, clear the
995+
// waiter signaled bit for this type of waiter if applicable, to indicate that a waiter of this type is no longer
996+
// signaled.
997+
//
998+
// If the waiter signaled bit is not updated upon event reset, the following scenario would lead to deadlock:
999+
// - Thread T0 signals the write waiter event or the upgradeable read waiter event to wake a waiter
1000+
// - There are no threads waiting on the event, but T1 is in WaitOnEvent() after exiting the spin lock and before
1001+
// actually waiting on the event (that is, it's recorded that there is one waiter for the event). It remains in
1002+
// this region for a while, in the repro case it typically gets context-switched out.
1003+
// - T2 acquires the RW lock in some fashion that blocks T0 or T3 from acquiring the RW lock
1004+
// - T0 or T3 fails to acquire the RW lock enough times for it to enter WaitOnEvent for the same event as T1
1005+
// - T0 or T3 resets the event
1006+
// - T2 releases the RW lock and does not wake a waiter because the reset at the previous step lost a signal but
1007+
// _waiterStates was not updated to reflect that
1008+
// - T1 and other threads begin waiting on the event, but there's no longer any thread that would wake them
1009+
if (waiterSignaledState != WaiterStates.None && (_waiterStates & waiterSignaledState) != WaiterStates.None)
1010+
{
1011+
_waiterStates &= ~waiterSignaledState;
1012+
}
9701013
waitEvent.Reset();
1014+
9711015
numWaiters++;
9721016
HasNoWaiters = false;
9731017

@@ -986,40 +1030,19 @@ private bool WaitOnEvent(
9861030
}
9871031
finally
9881032
{
989-
WaiterStates waiterSignaledState = WaiterStates.None;
990-
EnterSpinLockReason enterMyLockReason;
991-
switch (enterLockType)
992-
{
993-
case EnterLockType.UpgradeableRead:
994-
waiterSignaledState = WaiterStates.UpgradeableReadWaiterSignaled;
995-
goto case EnterLockType.Read;
996-
997-
case EnterLockType.Read:
998-
enterMyLockReason = EnterSpinLockReason.EnterAnyRead;
999-
break;
1000-
1001-
case EnterLockType.Write:
1002-
waiterSignaledState = WaiterStates.WriteWaiterSignaled;
1003-
enterMyLockReason = EnterSpinLockReason.EnterWrite;
1004-
break;
1005-
1006-
default:
1007-
Debug.Assert(enterLockType == EnterLockType.UpgradeToWrite);
1008-
enterMyLockReason = EnterSpinLockReason.UpgradeToWrite;
1009-
break;
1010-
}
10111033
_spinLock.Enter(enterMyLockReason);
10121034

10131035
--numWaiters;
10141036

1015-
if (waitSuccessful && waiterSignaledState != WaiterStates.None)
1037+
if (waitSuccessful &&
1038+
waiterSignaledState != WaiterStates.None &&
1039+
(_waiterStates & waiterSignaledState) != WaiterStates.None)
10161040
{
10171041
// Indicate that a signaled waiter of this type has woken. Since non-read waiters are signaled to wake one
10181042
// at a time, we avoid waking up more than one waiter of that type upon successive enter/exit loops until
10191043
// the signaled thread actually wakes up. For example, if there are multiple write waiters and one thread is
10201044
// repeatedly entering and exiting a write lock, every exit would otherwise signal a different write waiter
10211045
// to wake up unnecessarily when only one woken waiter may actually succeed in entering the write lock.
1022-
Debug.Assert((_waiterStates & waiterSignaledState) != WaiterStates.None);
10231046
_waiterStates &= ~waiterSignaledState;
10241047
}
10251048

0 commit comments

Comments
 (0)