public void AcquireReaderLock(int millisecondsTimeout)
{
if (millisecondsTimeout < -1)
{
throw GetInvalidTimeoutException(nameof(millisecondsTimeout));
}
ThreadLocalLockEntry threadLocalLockEntry = ThreadLocalLockEntry.GetOrCreateCurrent(_lockID);
// Check for the fast path
if (Interlocked.CompareExchange(ref _state, LockStates.Reader, 0) == 0)
{
Debug.Assert(threadLocalLockEntry._readerLevel == 0);
}
// Check for nested reader
else if (threadLocalLockEntry._readerLevel > 0)
{
Debug.Assert((_state & LockStates.ReadersMask) != 0);
if (threadLocalLockEntry._readerLevel == MaxAcquireCount)
{
throw new OverflowException(SR.Overflow_UInt16);
}
++threadLocalLockEntry._readerLevel;
return;
}
// Check if the thread already has writer lock
else if (_writerID == GetCurrentThreadID())
{
AcquireWriterLock(millisecondsTimeout);
Debug.Assert(threadLocalLockEntry.IsFree);
return;
}
else
{
int spinCount = 0;
int currentState = _state;
do
{
int knownState = currentState;
// Reader need not wait if there are only readers and no writer
if (knownState < LockStates.ReadersMask ||
(
(knownState & LockStates.ReaderSignaled) != 0 &&
(knownState & LockStates.Writer) == 0 &&
(
// A waiting reader, after successfully completing the wait, expects that it can become a
// reader, so ensure that there is enough room for waiting readers and this potential reader.
(
(knownState & LockStates.ReadersMask) +
((knownState & LockStates.WaitingReadersMask) >> LockStates.WaitingReadersShift)
) <= LockStates.ReadersMask - 2
)
))
{
// Add to readers
currentState = Interlocked.CompareExchange(ref _state, knownState + LockStates.Reader, knownState);
if (currentState == knownState)
{
// One more reader
break;
}
continue;
}
// Check for too many readers or waiting readers, or if signaling is in progress. The check for signaling
// prevents new readers from starting to wait for a read lock while the previous set of waiting readers are
// being granted their lock. This is necessary to guarantee thread safety for the 'finally' block below.
if ((knownState & LockStates.ReadersMask) == LockStates.ReadersMask ||
(knownState & LockStates.WaitingReadersMask) == LockStates.WaitingReadersMask ||
(knownState & LockStates.CachingEvents) == LockStates.ReaderSignaled)
{
// Sleep for a while, then update to the latest state and try again
int sleepDurationMilliseconds = 100;
if ((knownState & LockStates.ReadersMask) == LockStates.ReadersMask ||
(knownState & LockStates.WaitingReadersMask) == LockStates.WaitingReadersMask)
{
sleepDurationMilliseconds = 1000;
}
Helpers.Sleep(sleepDurationMilliseconds);
spinCount = 0;
currentState = _state;
continue;
}
++spinCount;
// Check if events are being cached. The purpose of this check is that "caching" events could involve
// disposing one or both of {_readerEvent, _writerEvent}. This check prevents the waiting code below from
// trying to use these events during this dangerous time, and instead causes the loop to spin until the
// caching state is cleared and events can be recreated. See ReleaseEvents() and callers.
if ((knownState & LockStates.CachingEvents) == LockStates.CachingEvents)
{
if (spinCount > DefaultSpinCount)
{
Helpers.Sleep(1);
spinCount = 0;
}
currentState = _state;
continue;
}
// Check spin count
if (spinCount <= DefaultSpinCount)
{
currentState = _state;
continue;
}
// Add to waiting readers
currentState = Interlocked.CompareExchange(ref _state, knownState + LockStates.WaitingReader, knownState);
if (currentState != knownState)
{
continue;
}
int modifyState = -LockStates.WaitingReader;
ManualResetEventSlim readerEvent = null;
bool waitSucceeded = false;
try
{
readerEvent = GetOrCreateReaderEvent();
waitSucceeded = readerEvent.Wait(millisecondsTimeout);
// AcquireReaderLock cannot have reentry via pumping while waiting for readerEvent, so
// threadLocalLockEntry's state should not change from underneath us
Debug.Assert(threadLocalLockEntry.HasLockID(_lockID));
if (waitSucceeded)
{
// Become a reader
Debug.Assert((_state & LockStates.ReaderSignaled) != 0);
Debug.Assert((_state & LockStates.ReadersMask) < LockStates.ReadersMask);
modifyState += LockStates.Reader;
}
}
finally
{
// Make the state changes determined above
knownState = Interlocked.Add(ref _state, modifyState) - modifyState;
if (!waitSucceeded)
{
// Check for last signaled waiting reader. This is a rare case where the wait timed out, but shortly
// afterwards, waiting readers got released, hence the ReaderSignaled bit is set. In that case,
// remove the ReaderSignaled bit from the state, acquire a read lock, and release it. While the
// ReaderSignaled bit is set, new requests for a write lock must spin or wait to acquire the lock,
// so it is safe for this thread to acquire a read lock and call ReleaseReaderLock() as a shortcut
// to do the work of releasing other waiters.
if ((knownState & LockStates.ReaderSignaled) != 0 &&
(knownState & LockStates.WaitingReadersMask) == LockStates.WaitingReader)
{
if (readerEvent == null)
{
readerEvent = _readerEvent;
Debug.Assert(readerEvent != null);
}
// Ensure the event is signaled before resetting it, since the ReaderSignaled state is set
// before the event is set.
readerEvent.Wait();
Debug.Assert((_state & LockStates.ReadersMask) < LockStates.ReadersMask);
// Reset the event and lower reader signaled flag
readerEvent.Reset();
Interlocked.Add(ref _state, LockStates.Reader - LockStates.ReaderSignaled);
// Honor the orginal status
++threadLocalLockEntry._readerLevel;
ReleaseReaderLock();
}
Debug.Assert(threadLocalLockEntry.IsFree);
}
}
if (!waitSucceeded)
{
throw GetTimeoutException();
}
// Check for last signaled waiting reader
Debug.Assert((knownState & LockStates.ReaderSignaled) != 0);
Debug.Assert((knownState & LockStates.ReadersMask) < LockStates.ReadersMask);
if ((knownState & LockStates.WaitingReadersMask) == LockStates.WaitingReader)
{
// Reset the event and the reader signaled flag
readerEvent.Reset();
Interlocked.Add(ref _state, -LockStates.ReaderSignaled);
}
break;
} while (YieldProcessor());
}
// Success
Debug.Assert((_state & LockStates.Writer) == 0);
Debug.Assert((_state & LockStates.ReadersMask) != 0);
++threadLocalLockEntry._readerLevel;
}