// ==++==
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// ==--==
// =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
//
// rtlocks.cpp
//
// Implementation file for locks used only within the runtime implementation. The locks
// themselves are expected to be dependent on the underlying platform definition.
//
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
#include "concrtinternal.h"
#pragma warning (disable : 4702)
namespace Concurrency
{
namespace details
{
const unsigned int SPIN_COUNT = 4000;
unsigned int _SpinCount::_S_spinCount = SPIN_COUNT;
static const long NotTriggered = 0;
static const long TriggeredByUnblock = 1;
static const long TriggeredByTimeout = 2;
#if defined(_DEBUG)
#define DebugBitsNone 0
#define DebugBitsMask 0xF0000000
///
/// Returns a set of debug bits indicating where the lock was acquired.
///
LONG GetDebugBits()
{
return DebugBitsNone;
}
///
/// Validates the lock conditions.
///
void ValidateDebugBits(LONG dbgBits)
{
(dbgBits);
}
#endif // _DEBUG
void _SpinCount::_Initialize()
{
_S_spinCount = (::Concurrency::GetProcessorCount() > 1) ? SPIN_COUNT : 0;
}
unsigned int _SpinCount::_Value()
{
return _S_spinCount;
}
//
// The non-reentrant lock for use with the thread-based implementation is defined as
// a 32-bit integer that is set to '1' when the lock is held, using interlocked
// APIs.
//
_NonReentrantBlockingLock::_NonReentrantBlockingLock()
{
static_assert(sizeof(CRITICAL_SECTION) <= sizeof(_M_criticalSection), "_M_critical section buffer too small");
CRITICAL_SECTION * pCriticalSection = reinterpret_cast(_M_criticalSection);
new(pCriticalSection) CRITICAL_SECTION;
platform::__InitializeCriticalSectionEx(pCriticalSection, _SpinCount::_S_spinCount);
}
_NonReentrantBlockingLock::~_NonReentrantBlockingLock()
{
CRITICAL_SECTION * pCriticalSection = reinterpret_cast(_M_criticalSection);
DeleteCriticalSection(pCriticalSection);
}
//
// Acquire the lock using an InterlockedExchange on _M_lock. After s_spinCount
// number of retries, it will begin calling sleep(0).
//
void _NonReentrantBlockingLock::_Acquire()
{
CRITICAL_SECTION * pCriticalSection = reinterpret_cast(_M_criticalSection);
EnterCriticalSection(pCriticalSection);
}
void _NonReentrantLock::_DebugAcquire()
{
#if defined(_DEBUG)
LONG old;
LONG dbgBits = GetDebugBits();
_SpinWaitBackoffNone spinWait(_Sleep0);
for (;;)
{
//
// Under the debug build, verify lock sharing rules in the runtime by stealing high bits of the _M_lock field.
// This is purely for UMS so we don't run into people changing lock structures and inadvertently causing HARD TO FIND
// random deadlocks in UMS.
//
old = _M_Lock;
if ((old & 1) == 0)
{
LONG destVal = old | 1 | dbgBits;
LONG xchg = InterlockedCompareExchange(&_M_Lock, destVal, old);
if (xchg == old)
{
ValidateDebugBits(destVal);
break;
}
}
spinWait._SpinOnce();
}
#endif // _DEBUG
}
//
// Try to acquire the lock, does not spin if it is unable to acquire.
//
bool _NonReentrantBlockingLock::_TryAcquire()
{
CRITICAL_SECTION * pCriticalSection = reinterpret_cast(_M_criticalSection);
return TryEnterCriticalSection(pCriticalSection) != 0;
}
bool _NonReentrantLock::_DebugTryAcquire()
{
#if defined(_DEBUG)
LONG dbgBits = GetDebugBits();
LONG old = _M_Lock;
if ((old & 1) == 0)
{
for(;;)
{
if ((old & 1) == 1)
break;
LONG destVal = old | 1 | dbgBits;
LONG xchg = InterlockedCompareExchange(&_M_Lock, destVal, old);
if (xchg == old)
{
ValidateDebugBits(destVal);
return true;
}
old = xchg;
}
}
#endif // _DEBUG
return false;
}
//
// Release the lock, which can be safely done without a memory barrier
//
void _NonReentrantBlockingLock::_Release()
{
CRITICAL_SECTION * pCriticalSection = reinterpret_cast(_M_criticalSection);
LeaveCriticalSection(pCriticalSection);
}
#define NULL_THREAD_ID -1L
_ReentrantBlockingLock::_ReentrantBlockingLock()
{
static_assert(sizeof(CRITICAL_SECTION) <= sizeof(_M_criticalSection), "_M_critical section buffer too small");
CRITICAL_SECTION * pCriticalSection = reinterpret_cast(_M_criticalSection);
new(pCriticalSection) CRITICAL_SECTION;
platform::__InitializeCriticalSectionEx(pCriticalSection, _SpinCount::_S_spinCount);
}
_ReentrantBlockingLock::~_ReentrantBlockingLock()
{
CRITICAL_SECTION * pCriticalSection = reinterpret_cast(_M_criticalSection);
DeleteCriticalSection(pCriticalSection);
}
_ReentrantLock::_ReentrantLock()
{
_M_owner = NULL_THREAD_ID;
_M_recursionCount = 0;
}
void _ReentrantBlockingLock::_Acquire()
{
CRITICAL_SECTION * pCriticalSection = reinterpret_cast(_M_criticalSection);
EnterCriticalSection(pCriticalSection);
}
void _ReentrantLock::_Acquire()
{
LONG id = (LONG) GetCurrentThreadId();
LONG old;
_SpinWaitBackoffNone spinWait(_Sleep0);
#if defined(_DEBUG)
LONG dbgBits = GetDebugBits();
#endif // _DEBUG
for (;;)
{
old = InterlockedCompareExchange(&_M_owner, id, NULL_THREAD_ID);
if ( old == NULL_THREAD_ID )
{
#if defined(_DEBUG)
//
// Under the debug build, verify lock sharing rules in the runtime by stealing high bits of the _M_recursionCount field.
// This is purely for UMS so we don't run into people changing lock structures and inadvertently causing HARD TO FIND
// random deadlocks in UMS.
//
// This does mean you better not recursively acquire the lock more than a billion times ;)
//
_M_recursionCount = (_M_recursionCount & DebugBitsMask) | 1;
#else // _DEBUG
_M_recursionCount = 1;
#endif // _DEBUG
break;
}
else if ( old == id )
{
#if defined(_DEBUG)
CONCRT_COREASSERT((_M_recursionCount & ~DebugBitsMask) < (DebugBitsMask - 2));
_M_recursionCount = ((_M_recursionCount & ~DebugBitsMask) + 1) | (_M_recursionCount & DebugBitsMask) | dbgBits;
#else
_M_recursionCount++;
#endif // _DEBUG
break;
}
spinWait._SpinOnce();
}
#if defined(_DEBUG)
ValidateDebugBits(_M_recursionCount);
#endif // _DEBUG
}
bool _ReentrantBlockingLock::_TryAcquire()
{
CRITICAL_SECTION * pCriticalSection = reinterpret_cast(_M_criticalSection);
return TryEnterCriticalSection(pCriticalSection) != 0;
}
bool _ReentrantLock::_TryAcquire()
{
#if defined(_DEBUG)
LONG dbgBits = GetDebugBits();
#endif // _DEBUG
LONG id = (LONG) GetCurrentThreadId();
LONG old = InterlockedCompareExchange(&_M_owner, id, NULL_THREAD_ID);
if ( old == NULL_THREAD_ID || old == id )
{
#if defined(_DEBUG)
CONCRT_COREASSERT((_M_recursionCount & ~DebugBitsMask) < (DebugBitsMask - 2));
_M_recursionCount = ((_M_recursionCount & ~DebugBitsMask) + 1) | (_M_recursionCount & DebugBitsMask) | dbgBits;
#else // !_DEBUG
_M_recursionCount++;
#endif
}
else
{
return false;
}
#if defined(_DEBUG)
ValidateDebugBits(_M_recursionCount);
#endif // _DEBUG
return true;
}
void _ReentrantBlockingLock::_Release()
{
CRITICAL_SECTION * pCriticalSection = reinterpret_cast(_M_criticalSection);
LeaveCriticalSection(pCriticalSection);
}
void _ReentrantLock::_Release()
{
if ( _M_owner != (LONG) GetCurrentThreadId() || _M_recursionCount < 1)
return;
#if defined(_DEBUG)
if ( (_M_recursionCount & ~DebugBitsMask) < 1 )
#else // !_DEBUG
if ( _M_recursionCount < 1 )
#endif // _DEBUG
return;
_M_recursionCount--;
#if defined(_DEBUG)
if ( (_M_recursionCount & DebugBitsMask) == 0 )
#else // !_DEBUG
if ( _M_recursionCount == 0 )
#endif // DEBUG
{
_M_owner = NULL_THREAD_ID;
}
}
//
// NonReentrant PPL Critical Section Wrapper
//
_NonReentrantPPLLock::_NonReentrantPPLLock()
{
}
void _NonReentrantPPLLock::_Acquire(void* _Lock_node)
{
_M_criticalSection._Acquire_lock(_Lock_node, true);
}
void _NonReentrantPPLLock::_Release()
{
_M_criticalSection.unlock();
}
//
// Reentrant PPL Critical Section Wrapper
//
_ReentrantPPLLock::_ReentrantPPLLock()
{
_M_owner = NULL_THREAD_ID;
_M_recursionCount = 0;
}
void _ReentrantPPLLock::_Acquire(void* _Lock_node)
{
LONG id = (LONG) GetCurrentThreadId();
if ( _M_owner == id )
{
_M_recursionCount++;
}
else
{
_M_criticalSection._Acquire_lock(_Lock_node, true);
_M_owner = id;
_M_recursionCount = 1;
}
}
void _ReentrantPPLLock::_Release()
{
ASSERT(_M_owner == (LONG) GetCurrentThreadId());
ASSERT(_M_recursionCount >= 1);
_M_recursionCount--;
if ( _M_recursionCount == 0 )
{
_M_owner = NULL_THREAD_ID;
_M_criticalSection.unlock();
}
}
//
// A Non-Reentrant Reader-Writer spin lock, designed for rare writers.
//
// A writer request immediately blocks future readers and then waits until all current
// readers drain. A reader request does not block future writers and must wait until
// all writers are done, even those that cut in front In any race between requesting
// and reader and a writer, the writer always wins.
//
_ReaderWriterLock::_ReaderWriterLock()
: _M_state(_ReaderWriterLock::_Free), _M_numberOfWriters(0)
{
}
//
// Acquires the RWLock for reading. Waits for the number of writers to drain.
//
void _ReaderWriterLock::_AcquireRead()
{
#if defined(_DEBUG)
LONG dbgBits = GetDebugBits();
LONG val = _M_numberOfWriters;
for(;;)
{
LONG xchgVal = InterlockedCompareExchange(&_M_numberOfWriters, val | dbgBits, val);
if (xchgVal == val)
break;
val = xchgVal;
}
#endif // _DEBUG
for (;;)
{
if (_M_numberOfWriters > 0)
#if defined(_DEBUG)
_WaitEquals(_M_numberOfWriters, 0, ~DebugBitsMask);
#else // !_DEBUG
_WaitEquals(_M_numberOfWriters, 0);
#endif // _DEBUG
int currentState = _M_state;
// Try to acquire read lock by incrementing the current State.
if (currentState != _Write &&
InterlockedCompareExchange(&_M_state, currentState + 1, currentState) == currentState)
{
#if defined(_DEBUG)
ValidateDebugBits(_M_numberOfWriters);
#endif // _DEBUG
return;
}
}
}
//
// Release read lock -- the last reader will decrement _M_state to _Free
//
void _ReaderWriterLock::_ReleaseRead()
{
ASSERT(_M_state >= _Read);
InterlockedDecrement(&_M_state);
}
//
// Acquire write lock -- spin until there are no existing readers, no new readers will
// be added
//
void _ReaderWriterLock::_AcquireWrite()
{
InterlockedIncrement(&_M_numberOfWriters);
for (;;)
{
if (InterlockedCompareExchange(&_M_state, _Write, _Free) == _Free)
{
#if defined(_DEBUG)
ValidateDebugBits(_M_numberOfWriters);
#endif // _DEBUG
return;
}
_WaitEquals(_M_state, _Free);
}
}
//
// Release writer lock -- there can only be one active, but a bunch might be pending
//
void _ReaderWriterLock::_ReleaseWrite()
{
ASSERT(_M_state == _Write);
#if defined(_DEBUG)
ASSERT((_M_numberOfWriters & ~DebugBitsMask) > 0);
#else // !_DEBUG
ASSERT(_M_numberOfWriters > 0);
#endif // _DEBUG
// The following assignment does not need to be interlocked, as the interlocked
// decrement can take care of the fence.
_M_state = _Free;
InterlockedDecrement(&_M_numberOfWriters);
}
//
// Tries to acquire the write lock. Returns true if the lock was acquired.
//
bool _ReaderWriterLock::_TryAcquireWrite()
{
if (InterlockedCompareExchange(&_M_state, _Write, _Free) == _Free)
{
InterlockedIncrement(&_M_numberOfWriters);
#if defined(_DEBUG)
ValidateDebugBits(_M_numberOfWriters);
#endif // _DEBUG
return true;
}
return false;
}
// Spin-Wait-Until variant -- spin for s_spinCount iterations, then Sleep(0) then repeat
// 10 times (tunable), thereafter we spin and Sleep(1)
void _ReaderWriterLock::_WaitEquals(volatile const LONG& location, LONG value, LONG mask)
{
unsigned int retries = 0;
int spinInterval = 10; // tuning
for (;;)
{
if ((location & mask) == value)
return;
YieldProcessor();
if (++retries >= _SpinCount::_S_spinCount)
{
if (spinInterval > 0)
{
--spinInterval;
platform::__Sleep(0);
}
else
platform::__Sleep(1);
retries = 0;
}
}
}
// Guarantees that all writers are out of the lock. This does nothing if there are no pending writers.
void _ReaderWriterLock::_FlushWriteOwners()
{
//
// Ideally, if the read lock is held and we have pending writers, this would not need to grab the lock and release
// it; however -- we must guarantee that any writer which was in the lock as of this call is completely out
// of everything including _ReleaseWrite. Since the last thing which happens there is the decrement of _M_numberOfWriters,
// that is *currently* what we must key off. It's possible that after the change of _M_state to free there, a reader
// gets the lock because it was preempted after the initial check of _M_numberOfWriters which saw 0. Hence, we cannot
// rely on _M_state.
//
if (_M_numberOfWriters > 0)
{
#if defined(_DEBUG)
_WaitEquals(_M_numberOfWriters, 0, ~DebugBitsMask);
#else // !_DEBUG
_WaitEquals(_M_numberOfWriters, 0);
#endif // _DEBUG
}
}
//***************************************************************************
// Locking primitives and structures:
//***************************************************************************
// Reader-writer lock constants
static const long RWLockWriterInterested = 0x1; // Writer interested or active
static const long RWLockWriterExclusive = 0x2; // Writer active, no reader entry
static const long RWLockReaderInterested = 0x4; // Reader interested but not active
static const long RWLockReaderCountIncrement = 0x8; // Reader count step (reader counter is scaled by it)
///
/// Node element used in the lock queues.
///
class LockQueueNode
{
public:
///
/// Constructor for queue node. It keeps the context pointer in order
/// to block in a fashion visible to ConcRT.
///
LockQueueNode(unsigned int timeout = COOPERATIVE_TIMEOUT_INFINITE)
: m_pNextNode(NULL), m_ticketState(StateIsBlocked), m_hTimer(NULL), m_trigger(NotTriggered), m_fTimedNodeInvalid(0)
{
m_pContext = SchedulerBase::CurrentContext();
if (timeout != COOPERATIVE_TIMEOUT_INFINITE)
{
if ((m_hTimer = RegisterAsyncTimerAndLoadLibrary(timeout, LockQueueNode::DispatchNodeTimeoutTimer, this)) == nullptr)
{
//
// Note that the thread is left in a state unexplicable by the scheduler here. It's quite possible someone ::Unblocks this context in
// the future. With this error, we make no attempt to unwind that.
//
throw std::bad_alloc();
}
}
}
///
/// Constructor for queue node. It keeps the context pointer in order
/// to block in a fashion visible to ConcRT.
///
LockQueueNode(Context * pContext, unsigned int ticket)
: m_pContext(pContext), m_pNextNode(NULL), m_ticketState(ticket), m_hTimer(NULL), m_trigger(NotTriggered), m_fTimedNodeInvalid(0)
{
}
///
/// Waits until lock is available.
///
///
/// The number of the node that is currently owning the lock, or has last owned it.
///
void Block(unsigned int currentTicketState = 0)
{
// Get the number of physical processors to determine the best spin times
unsigned int numberOfProcessors = Concurrency::GetProcessorCount();
_CONCRT_ASSERT(numberOfProcessors > 0);
// If the previous node is blocked then there is no need to spin and waste cycles
if (!IsPreviousBlocked())
{
// If there is a race and the ticket is not valid then use the default spin
unsigned int placeInLine = IsTicketValid() ? ((m_ticketState >> NumberOfBooleanStates) - (currentTicketState >> NumberOfBooleanStates)) : 1;
_CONCRT_ASSERT(placeInLine > 0);
//
// If the node is back in line by more than a processor count plus a threshold
// then simply don't spin and block immediately. Otherwise, progressively increase the
// amount of spin for the subsequent nodes until a double default spin count is reached.
//
if (placeInLine <= numberOfProcessors + TicketThreshold)
{
const unsigned int defaultSpin = _SpinCount::_Value();
unsigned int totalSpin = defaultSpin + (defaultSpin * (placeInLine - 1)) / (numberOfProcessors + TicketThreshold);
_SpinWaitNoYield spinWait;
spinWait._SetSpinCount(totalSpin);
while (IsBlocked() && spinWait._SpinOnce())
{
// _YieldProcessor is called inside _SpinOnce
}
}
}
//
// After spin waiting for a while use the ConcRT blocking mechanism. It will return
// immediately if the unblock already happened.
//
m_pContext->Block();
}
///
/// Notifies that lock is available without context blocking.
///
void UnblockWithoutContext()
{
m_ticketState = m_ticketState & ~StateIsBlocked;
}
///
/// Compensate timer's unblock action, if necessary.
///
void TryCompensateTimer()
{
// No matter the timer win the race or not, this thread always get unblocked (It actually never get blocked)
// However, if the timer win the race, we do need to compensate its context::unblock.
if (m_hTimer && InterlockedExchange(&m_trigger, TriggeredByUnblock) == TriggeredByTimeout)
{
// Timer won the race for the trigger and has unblocked this context - block to consume the unblock.
m_pContext->Block();
// The thread that failed to trigger the waiting context needs to release the reference
DerefTimerNode();
}
}
///
/// Notifies that lock is available.
///
bool Unblock()
{
if (InterlockedCompareExchange(&m_trigger, TriggeredByUnblock, NotTriggered) == NotTriggered)
{
// The unblock has won the race (if this was a timed node)
UnblockWithoutContext();
//
// This call implies a fence which serves two purposes:
// a) it makes m_fIsBlocked visible sooner (in UnblockWithoutContext)
// b) it makes sure that we never block a context without unblocking it
//
m_pContext->Unblock();
return true;
}
return false;
}
///
/// Waits until the next node is set.
///
///
/// The next node.
///
LockQueueNode * WaitForNextNode()
{
LockQueueNode * pNextNode = m_pNextNode;
_SpinWaitBackoffNone spinWait;
while (pNextNode == NULL)
{
//
// There in no context blocking here so continue to spin even if maximum
// spin is already reached. Since setting the tail and setting next pointer
// are back-to-back operations it is very likely that while loop will not take
// a long time.
//
spinWait._SpinOnce();
pNextNode = m_pNextNode;
}
return pNextNode;
}
///
/// Copies the contents of the passed in node to this node.
///
///
/// The node copy from.
///
///
/// Used only to transfer data to the internally allocated node.
///
void Copy(LockQueueNode * pCopyFromNode)
{
_CONCRT_ASSERT(pCopyFromNode->IsTicketValid());
_CONCRT_ASSERT(!pCopyFromNode->IsBlocked());
m_ticketState = pCopyFromNode->m_ticketState;
m_pNextNode = pCopyFromNode->m_pNextNode;
m_pContext = pCopyFromNode->m_pContext;
}
///
/// Estimates the position of this node in the node queue based on the previous node.
///
///
/// The node to get the base number from, if available.
///
///
/// Used only as a heuristic for critical section and writers in reader writer lock.
///
void UpdateQueuePosition(LockQueueNode * pPreviousNode)
{
if (!IsTicketValid())
{
// If the previous node has a valid ticket then this one will have it as well
if (pPreviousNode->IsTicketValid())
{
unsigned int newState = (pPreviousNode->m_ticketState + TicketIncrement) & MaskBlockedStates;
_CONCRT_ASSERT((newState & StateIsTicketValid) != 0);
// If the previous node is blocked then set this information on the current node to save the spin
// We disabled the IsSynchronouslyBlocked check for timed-node. It is designed for work around an AV caused by accessing
// deleted context (After timer unblocking the thread, the thread may already exited and deleted its external context).
if (pPreviousNode->IsBlocked() && (pPreviousNode->IsPreviousBlocked() || pPreviousNode->m_hTimer == nullptr && pPreviousNode->m_pContext->IsSynchronouslyBlocked()))
{
newState |= StateIsPreviousBlocked;
}
m_ticketState = m_ticketState | newState;
}
}
}
///
/// Estimates the state of this node based on the state of previous node.
///
///
/// The node to get the base from, if available.
///
///
/// Used only as a heuristic for readers in reader writer lock.
///
void UpdateBlockingState(LockQueueNode * pPreviousNode)
{
// If the previous node is blocked then set this information on the current node to save the spin
// We don't need to check the m_hTimer here as UpdateQueuePosition because it is only used by read_write_lock, which does not have a timer.
if (pPreviousNode->IsBlocked() && (pPreviousNode->IsPreviousBlocked() || pPreviousNode->m_pContext->IsSynchronouslyBlocked()))
{
m_ticketState = m_ticketState | StateIsPreviousBlocked;
}
}
///
/// Timed waiting node (m_hTimer != nullptr) is allocated on the heap, and needs to be
/// released after use.
///
///
/// There are 2 references:
/// 1. The thread that waits on or attempts to wait on the block.
/// 2. The one who loses the race to trigger the waiting thread (this could be the timer callback,
/// the previous lock owner, or the waiting thread itself in the case where the lock was not already acquired).
///
void DerefTimerNode()
{
if (m_hTimer && InterlockedIncrement(&m_fTimedNodeInvalid) == 2)
{
delete this;
}
}
///
/// Called when a timer on an event is signaled.
///
static void CALLBACK DispatchNodeTimeoutTimer(PTP_CALLBACK_INSTANCE instance, void * pContext, PTP_TIMER timer)
{
LockQueueNode *pNode = reinterpret_cast (pContext);
if (InterlockedCompareExchange(&pNode->m_trigger, TriggeredByTimeout, NotTriggered) == NotTriggered)
{
pNode->m_pContext->Unblock();
}
else
{
// Unblock won the race, since we failed to trigger we're responsible for releasing the reference on the node.
pNode->DerefTimerNode();
}
// Always delete the timer
UnRegisterAsyncTimerAndUnloadLibrary(instance, timer);
}
///
/// Same as DispatchNodeTimeoutTimer, but used only for XP and MSDK
///
static void CALLBACK DispatchNodeTimeoutTimerXP(PVOID pContext, BOOLEAN)
{
LockQueueNode *pNode = reinterpret_cast (pContext);
// Always delete the timer
platform::__DeleteTimerQueueTimer(GetSharedTimerQueue(), pNode->m_hTimer, NULL);
if (InterlockedCompareExchange(&pNode->m_trigger, TriggeredByTimeout, NotTriggered) == NotTriggered)
{
pNode->m_pContext->Unblock();
}
else
{
// Unblock won the race, since we failed to trigger we're responsible for releasing the reference on the node.
pNode->DerefTimerNode();
}
}
private:
friend class critical_section;
friend class reader_writer_lock;
bool IsBlocked()
{
return (m_ticketState & StateIsBlocked) != 0;
}
bool IsPreviousBlocked()
{
return (m_ticketState & StateIsPreviousBlocked) != 0;
}
bool IsTicketValid()
{
return (m_ticketState & StateIsTicketValid) != 0;
}
// Const statics needed for blocking heuristics
static const unsigned int TicketThreshold = 2;
static const unsigned int StateIsBlocked = 0x00000001;
static const unsigned int StateIsTicketValid = 0x00000002;
static const unsigned int StateIsPreviousBlocked = 0x00000004;
static const unsigned int MaskBlockedStates = ~(StateIsBlocked | StateIsPreviousBlocked);
static const unsigned int NumberOfBooleanStates = 0x00000003;
static const unsigned int TicketIncrement = 1 << NumberOfBooleanStates;
Context * m_pContext;
LockQueueNode * volatile m_pNextNode;
volatile unsigned int m_ticketState;
// Timer handle (valid only for XP; on Vista and above, this handle only used as the indication of whether current node is a timed node.
HANDLE m_hTimer;
// The trigger - for timed waits, the unblock mechanism competes with the timer to trigger the thread attempting to acquire the lock.
// Note, the acquiring thread may fire the trigger itself if the lock is not held - this counts as a virtual 'unblock'
volatile long m_trigger;
volatile long m_fTimedNodeInvalid;
};
//
// A C++ holder for a Non-reentrant PPL lock.
//
_CONCRTIMP _NonReentrantPPLLock::_Scoped_lock::_Scoped_lock(_NonReentrantPPLLock & _Lock) : _M_lock(_Lock)
{
new(reinterpret_cast (_M_lockNode)) LockQueueNode;
_M_lock._Acquire(reinterpret_cast (_M_lockNode));
}
_CONCRTIMP _NonReentrantPPLLock::_Scoped_lock::~_Scoped_lock()
{
_M_lock._Release();
}
//
// A C++ holder for a Reentrant PPL lock.
//
_CONCRTIMP _ReentrantPPLLock::_Scoped_lock::_Scoped_lock(_ReentrantPPLLock & _Lock) : _M_lock(_Lock)
{
new(reinterpret_cast (_M_lockNode)) LockQueueNode;
_M_lock._Acquire(reinterpret_cast (_M_lockNode));
}
_CONCRTIMP _ReentrantPPLLock::_Scoped_lock::~_Scoped_lock()
{
_M_lock._Release();
}
} // namespace details
///
/// Constructs an critical section
///
_CONCRTIMP critical_section::critical_section() : _M_pHead(NULL), _M_pTail(NULL)
{
_CONCRT_ASSERT(sizeof(_M_activeNode) >= sizeof(LockQueueNode));
// Hide the inside look of LockQueueNode behind a char array big enough to keep 3 pointers
// This is why LockQueueNode is newed in place instead of a more traditional allocation.
new(reinterpret_cast(_M_activeNode)) LockQueueNode(NULL, LockQueueNode::StateIsTicketValid);
}
///
/// Destroys a critical section. It is expected that the lock is no longer held.
///
_CONCRTIMP critical_section::~critical_section()
{
_ASSERT_EXPR(_M_pHead == NULL, L"Lock was destructed while held");
}
///
/// Gets a critical section handle.
///
///
/// A reference to this critical section.
///
_CONCRTIMP critical_section::native_handle_type critical_section::native_handle()
{
return *this;
}
///
/// Acquires this critical section.
///
///
/// Throws a improper_lock exception if the lock is acquired recursively
///
_CONCRTIMP void critical_section::lock()
{
LockQueueNode newNode; // Allocated on the stack and goes out of scope before unlock()
LockQueueNode * pNewNode = &newNode;
//
// Acquire the lock node that was just created on the stack
//
_Acquire_lock(pNewNode, false);
//
// At this point the context has exclusive ownership of the lock
//
_Switch_to_active(pNewNode);
}
///
/// Tries to acquire the lock, does not block.
///
///
/// true if the lock is acquired, false otherwise
///
_CONCRTIMP bool critical_section::try_lock()
{
LockQueueNode newNode; // Allocated on the stack and goes out of scope before unlock()
LockQueueNode * pNewNode = &newNode;
LockQueueNode * pPreviousNode = reinterpret_cast(InterlockedCompareExchangePointer(&_M_pTail, pNewNode, NULL));
// Try and acquire this lock. If this CAS succeeds, then the lock has been acquired.
if (pPreviousNode == NULL)
{
_M_pHead = pNewNode;
pNewNode->UpdateQueuePosition(reinterpret_cast(_M_activeNode));
pNewNode->UnblockWithoutContext();
_Switch_to_active(pNewNode);
return true;
}
return false;
}
///
/// Tries to acquire the lock for timeout number of milliseconds, does not block.
///
///
/// true if the lock is acquired, false otherwise
///
_CONCRTIMP bool critical_section::try_lock_for(unsigned int timeout)
{
LockQueueNode * pNewNode = new LockQueueNode(timeout); // Allocated on the heap
//
// Acquire the lock node that was just created
//
if (_Acquire_lock(pNewNode, false))
{
//
// At this point the context has exclusive ownership of the lock
//
_Switch_to_active(pNewNode);
pNewNode->DerefTimerNode();
return true;
}
else
{
pNewNode->DerefTimerNode();
return false;
}
}
///
/// Unlocks an acquired lock.
///
_CONCRTIMP void critical_section::unlock()
{
LockQueueNode * pCurrentNode = reinterpret_cast(_M_pHead);
_ASSERT_EXPR(pCurrentNode != nullptr, L"Lock not being held");
_ASSERT_EXPR(pCurrentNode->m_pContext == SchedulerBase::SafeFastCurrentContext(), L"Lock being held by different context");
// Reset the context on the active node to ensure that it is possible to detect the error case
// where the same context tries to enter the lock twice. (enjoy the fence below)
reinterpret_cast(&_M_activeNode)->m_pContext = nullptr;
LockQueueNode * pNextNode = pCurrentNode->m_pNextNode;
// This assignment must be put before ICEP(_M_pTail) because
// 1. It must be visible before ICEP(_M_pTail), and
// 2. It must be visible before unblock.
_M_pHead = pNextNode;
// If we reach the end of the queue of waiters, we need to handle a potential race with new incoming waiters.
if (pNextNode == nullptr && reinterpret_cast(InterlockedCompareExchangePointer(&_M_pTail, nullptr, pCurrentNode)) != pCurrentNode)
{
// If someone is adding a context then wait until next node pointer is populated.
pNextNode = pCurrentNode->WaitForNextNode();
// DO NOT try to combine this assignment with the one above by moving it out of and after the if statement. Moving it to after the if statement
// could lead to situations where the set of _M_pHead is not fenced.
_M_pHead = pNextNode;
}
// It's no longer safe to touch pNextNode after it gets unblocked
while (pNextNode != nullptr && !pNextNode->Unblock())
{
// The unblock could only have failed because the block is a timed block and was woken up by the timer.
pCurrentNode = pNextNode;
pNextNode = pCurrentNode->m_pNextNode;
// This assignment must be put before ICEP(_M_pTail) below
_M_pHead = pNextNode;
// If we reach the tail end of the waiters queue, we need to handle a potential race due to a new waiter being added to the tail.
if (pNextNode == nullptr && reinterpret_cast(InterlockedCompareExchangePointer(&_M_pTail, nullptr, pCurrentNode)) != pCurrentNode)
{
// If someone is adding a context then wait until next node pointer is populated.
pNextNode = pCurrentNode->WaitForNextNode();
_M_pHead = pNextNode;
}
// Since this was a timer node that was released by the timer firing we need to release our reference on it so it can be deleted.
pCurrentNode->DerefTimerNode();
}
}
///
/// If no one owns the lock at the instant the API is called, it returns instantly. If there is an owner,
/// it performs a lock followed by an unlock.
///
void critical_section::_Flush_current_owner()
{
if (_M_pTail != NULL)
{
lock();
unlock();
}
}
///
/// Acquires this critical section given a specific node to lock.
///
///
/// The node that needs to own the lock.
///
///
/// Returns true if the lock was acquired, false if the timer woke us up.
///
///
/// Throws a improper_lock exception if the lock is acquired recursively
///
bool critical_section::_Acquire_lock(void * _PLockingNode, bool _FHasExternalNode)
{
LockQueueNode * pNewNode = reinterpret_cast(_PLockingNode);
LockQueueNode * pActiveNode = reinterpret_cast(&_M_activeNode);
// Locks are non-reentrant, so throw if this condition is detected.
if (pNewNode->m_pContext == pActiveNode->m_pContext)
{
throw improper_lock("Lock already taken");
}
LockQueueNode * pPrevious = reinterpret_cast(InterlockedExchangePointer(&_M_pTail, pNewNode));
// No one held this critical section, so this context now acquired the lock
if (pPrevious == NULL)
{
_M_pHead = pNewNode;
pNewNode->UpdateQueuePosition(pActiveNode);
pNewNode->UnblockWithoutContext();
// If this was a timed wait, we need to compensate for the fact that the timer callback may race with us,
// and try to unblock this context. If that happens we need to invoke Context::Block to consume the unblock
// or the state on the context will be invalid.
pNewNode->TryCompensateTimer();
}
else
{
pNewNode->UpdateQueuePosition(pPrevious);
pPrevious->m_pNextNode = pNewNode;
// NOT SAFE TO TOUCH pPrevious AFTER THE ASSIGNMENT ABOVE!
pNewNode->Block(pActiveNode->m_ticketState);
// Do another position estimation in case we missed the previous number due to race if we've acquired the lock.
if (pNewNode->m_trigger != TriggeredByTimeout)
{
pNewNode->UpdateQueuePosition(pActiveNode);
}
}
// Since calls with external nodes will not call _Switch_to_active, make
// sure that we are setting the head and the active node properly.
if (_FHasExternalNode)
{
pActiveNode->Copy(pNewNode);
_M_pHead = pNewNode;
}
// The block can be in the NotTriggered state if it is not a timed block and the lock was not already held.
return pNewNode->m_trigger != TriggeredByTimeout;
}
///
/// The acquiring node allocated on the stack never really owns the lock. The reason for that is that
/// it would go out of scope and its insides would not be visible in unlock() where it would potentially
/// need to unblock the next in the queue. Instead, its state is transferred to the internal
/// node which is used as a scratch node.
///
///
/// The node that needs to own the lock.
///
void critical_section::_Switch_to_active(void * _PLockingNode)
{
LockQueueNode * pLockingNode = reinterpret_cast(_PLockingNode);
LockQueueNode * pActiveNode = reinterpret_cast(&_M_activeNode);
//
// Copy the contents of the node allocated on the stack which now owns the lock, so that we would
// have its information available during unlock.
//
pActiveNode->Copy(pLockingNode);
//
// If someone is acquiring the critical_section then wait until next node pointer is populated. Otherwise, there will be no way
// to unblock that acquiring context after pLockingNode goes out of scope.
//
if (pActiveNode->m_pNextNode == NULL)
{
//
// If the compare-and-swap to active node succeeds that means that a new acquirer coming in will
// properly set the _M_pHead. Otherwise, it has to be set manually when next node is done.
//
if (reinterpret_cast(InterlockedCompareExchangePointer(&_M_pTail, pActiveNode, pLockingNode)) != pLockingNode)
{
pLockingNode->WaitForNextNode();
//
// During the initial copy the next pointer was not copied over and it has been populated in the meantime.
// This copy can now be safely performed because tail has moved, so next will point to the second element.
//
pActiveNode->Copy(pLockingNode);
}
}
_CONCRT_ASSERT(_PLockingNode != _M_pTail);
_M_pHead = pActiveNode;
}
///
/// Constructs a holder object and acquires the critical_section passed to it.
// If the critical_section is held by another thread this call will block.
///
///
/// Critical section to lock.
///
critical_section::scoped_lock::scoped_lock(critical_section& _Critical_section) : _M_critical_section(_Critical_section)
{
static_assert(sizeof(LockQueueNode) <= sizeof(_M_node), "_M_node buffer too small");
LockQueueNode * pNewNode = reinterpret_cast(_M_node);
new(pNewNode) LockQueueNode;
_M_critical_section._Acquire_lock(pNewNode, true);
}
///
/// Destructs a holder object and releases the critical_section.
///
critical_section::scoped_lock::~scoped_lock()
{
_M_critical_section.unlock();
}
///
/// Constructs a new reader_writer_lock object.
///
_CONCRTIMP reader_writer_lock::reader_writer_lock() : _M_pReaderHead(NULL), _M_pWriterHead(NULL), _M_pWriterTail(NULL), _M_lockState(0)
{
_CONCRT_ASSERT(sizeof(_M_activeWriter) >= sizeof(LockQueueNode));
// Hide the inside look of LockQueueNode behind a char array big enough to keep 3 pointers
// This is why LockQueueNode is newed in place instead of a more traditional allocation.
new(reinterpret_cast (_M_activeWriter)) LockQueueNode(NULL, LockQueueNode::StateIsTicketValid);
}
///
/// Destructs reader_writer_lock object. If lock is held during the destruction an exception is thrown.
///
_CONCRTIMP reader_writer_lock::~reader_writer_lock()
{
_ASSERT_EXPR(_M_lockState == 0, L"Lock was destructed while held");
// Since LockQueueNode has a trivial destructor, no need to call it here. If it ever becomes
// non-trivial then it would be called here instead of calling delete (since memory is allocated
// in the char array and will be reclaimed anyway when reader_writer_lock is destructed).
}
///
/// Writer entering the lock. If there are readers active they are immediately notified to finish
/// and relinquish the lock.
///
///
/// Writer blocks by doing spinning on a local variable. Writers are chained so that a writer
/// exiting the lock releases the next writer in line.
///
_CONCRTIMP void reader_writer_lock::lock()
{
LockQueueNode newWriterNode; // Allocated on the stack and goes out of scope before unlock()
LockQueueNode * pNewWriter = &newWriterNode;
//
// Acquire the lock node that was just created on the stack
//
_Acquire_lock(pNewWriter, false);
//
// At this point the writer has exclusive ownership of the lock
//
_Switch_to_active(pNewWriter);
}
///
/// Try to take a writer lock.
///
///
/// true if the lock is immediately available and lock succeeded; false otherwise.
///
_CONCRTIMP bool reader_writer_lock::try_lock()
{
LockQueueNode newWriterNode; // Allocated on the stack and goes out of scope before unlock()
LockQueueNode * pNewWriter = &newWriterNode;
LockQueueNode * pPreviousWriter = reinterpret_cast(InterlockedCompareExchangePointer(&_M_pWriterTail, pNewWriter, NULL));
// Is this the only writer present? If yes, it will win over any new writer coming in.
if (pPreviousWriter == NULL)
{
_M_pWriterHead = pNewWriter;
// Is there any active readers? If no, our lock succeeded.
if (InterlockedCompareExchange(&_M_lockState, (RWLockWriterInterested | RWLockWriterExclusive), 0) == 0)
{
pNewWriter->UpdateQueuePosition(reinterpret_cast(_M_activeWriter));
pNewWriter->UnblockWithoutContext();
_Switch_to_active(pNewWriter);
return true;
}
else
{
// Lock failed, but other writers may now be linked to this failed write attempt.
// Thus, unwind all the actions and leave the lock in a consistent state.
_Remove_last_writer(pNewWriter);
}
}
return false;
}
///
/// Reader entering the lock. If there are writers active readers have to wait until they are done.
/// Reader simply registers an interest in the lock and waits for writers to release it.
///
///
/// Reader blocks by doing spinning on a local variable. All readers cache previous reader (if available)
/// locally, so they could all be unblocked once the lock is available.
///
_CONCRTIMP void reader_writer_lock::lock_read()
{
LockQueueNode newReaderNode;
LockQueueNode * pNewReader = &newReaderNode;
// Locks are non-reentrant, so throw if this condition is detected.
if (pNewReader->m_pContext == reinterpret_cast(_M_activeWriter)->m_pContext)
{
throw improper_lock("Lock already taken as a writer");
}
LockQueueNode * pNextReader = reinterpret_cast(InterlockedExchangePointer(&_M_pReaderHead, pNewReader));
//
// If this is the only read that currently exists and there are no interested writers
// then unblock this read.
//
if (pNextReader == NULL)
{
if ((InterlockedOr(&_M_lockState, RWLockReaderInterested) & (RWLockWriterInterested | RWLockWriterExclusive)) == 0)
{
LockQueueNode * pHeadReader = reinterpret_cast(_Get_reader_convoy());
//
// If the new reader is still the head of the reader list that means that it is
// unblocking itself, in which case using UnblockWithoutContext will not include
// context unblocking. Otherwise, the full unblock/block mechanism is needed.
//
if (pHeadReader == pNewReader)
{
pHeadReader->UnblockWithoutContext();
return;
}
_CONCRT_ASSERT(pHeadReader != pNewReader);
pHeadReader->Unblock();
}
}
else
{
pNewReader->UpdateBlockingState(pNextReader);
}
pNewReader->Block();
// Unblock the reader that preceeded this one as a head or the list
if (pNextReader != NULL)
{
InterlockedExchangeAdd(&_M_lockState, RWLockReaderCountIncrement);
pNextReader->Unblock();
}
}
///
/// Try to take a reader lock.
///
///
/// true if the lock is immediately available and lock succeeded; false otherwise.
///
_CONCRTIMP bool reader_writer_lock::try_lock_read()
{
long oldState = _M_lockState;
//
// Try to increment the reader count while no writer is interested.
//
while ((oldState & (RWLockWriterInterested | RWLockWriterExclusive)) == 0)
{
if (InterlockedCompareExchange(&_M_lockState, oldState + RWLockReaderCountIncrement, oldState) == oldState)
{
return true;
}
oldState = _M_lockState;
}
return false;
}
///
/// Unlock the lock based on who locked it, reader or writer.
///
_CONCRTIMP void reader_writer_lock::unlock()
{
if (_M_lockState >= RWLockReaderCountIncrement)
{
_Unlock_reader();
}
else if ((_M_lockState & RWLockWriterExclusive) != 0)
{
_Unlock_writer();
}
else
{
_ASSERT_EXPR(false, L"Lock not being held");
}
}
///
/// Called for the first context in the writer queue. It sets the queue head and it tries to
/// claim the lock if readers are not active.
///
///
/// The first writer in the queue.
///
bool reader_writer_lock::_Set_next_writer(void * _PWriter)
{
_M_pWriterHead = _PWriter;
if (((InterlockedOr(&_M_lockState, RWLockWriterInterested) & RWLockReaderInterested) == 0) &&
(InterlockedOr(&_M_lockState, RWLockWriterExclusive) < RWLockReaderCountIncrement))
{
return true;
}
return false;
}
///
/// Called when writers are done with the lock, or when lock was free for claiming by
/// the first reader coming in. If in the meantime there are more writers interested
/// the list of readers is finalized and they are convoyed, while head of the list
/// is reset to NULL.
///
///
/// Pointer to the head of the reader list.
///
void * reader_writer_lock::_Get_reader_convoy()
{
// In one interlocked step, clear reader interested flag and increment the reader count.
long prevLockState = InterlockedExchangeAdd(&_M_lockState, RWLockReaderCountIncrement - RWLockReaderInterested);
//
// If a lock is in the race between a reader and a writer allow this last reader batch
// to go through and then close the lock for the new incoming readers, granting
// exclusive access to writers.
//
if ((prevLockState & RWLockWriterInterested) != 0 && (prevLockState & RWLockWriterExclusive) == 0)
{
InterlockedOr(&_M_lockState, RWLockWriterExclusive);
}
// Return the batch of readers to be unblocked
return reinterpret_cast(InterlockedExchangePointer(&_M_pReaderHead, NULL));
}
///
/// Called from unlock() when a writer is holding the lock. Writer unblocks the next writer in the list
/// and is being retired. If there are no more writers, but there are readers interested, then readers
/// are unblocked.
///
///
/// If there wasn't for a race to add a writer while the last writer is unlocking the lock, there would be
/// no need for the writer structure in unlock. However, because of this race there is an ABA problem and
/// writer information had to be passed onto a scratch writer (_M_activeWriter), internal to the lock.
///
void reader_writer_lock::_Unlock_writer()
{
_CONCRT_ASSERT((_M_lockState & RWLockWriterExclusive) != 0);
_CONCRT_ASSERT(_M_pWriterHead != NULL);
LockQueueNode * pCurrentNode = reinterpret_cast(_M_pWriterHead);
_ASSERT_EXPR(pCurrentNode->m_pContext == SchedulerBase::SafeFastCurrentContext(), L"Lock being held by different writer");
LockQueueNode * pNextNode = pCurrentNode->m_pNextNode;
_M_pWriterHead = pNextNode;
// Reset context on the active writer to ensure that it is possible to detect the error case
// where the same writer tries to enter the lock twice.
reinterpret_cast(&_M_activeWriter)->m_pContext = NULL;
if (pNextNode != NULL)
{
pNextNode->Unblock();
}
else
{
// If there are readers lined up, then unblock them
if ((InterlockedAnd(&_M_lockState, ~(RWLockWriterInterested | RWLockWriterExclusive)) & RWLockReaderInterested) != 0)
{
LockQueueNode * pHeadNode = reinterpret_cast(_Get_reader_convoy());
pHeadNode->Unblock();
}
// Safely remove this writer, keeping in mind there might be a race for the queue tail.
_Remove_last_writer(pCurrentNode);
}
}
///
/// When last writer leaves the lock it needs to reset the tail to NULL so that the next coming
/// writer would know to try to grab the lock. If the CAS to NULL fails, then some other writer
/// managed to grab the tail before the reset, so this writer needs to wait until the link to
/// the next writer is complete before trying to release the next writer.
///
///
/// Last writer in the queue.
///
void reader_writer_lock::_Remove_last_writer(void * _PWriter)
{
// If someone is adding a writer then wait until next node pointer is populated.
if (reinterpret_cast(InterlockedCompareExchangePointer(&_M_pWriterTail, NULL, _PWriter)) != _PWriter)
{
LockQueueNode * pWriter = reinterpret_cast(_PWriter);
LockQueueNode * pNextWriter = pWriter->WaitForNextNode();
if (_Set_next_writer(pNextWriter))
{
pNextWriter->Unblock();
}
}
}
///
/// Acquires a write lock given a specific write node to lock.
///
///
/// The node that needs to own the lock.
///
///
/// Whether the node being locked is external to the reader_writer_lock.
///
///
/// Throws a improper_lock exception if the lock is acquired recursively
///
void reader_writer_lock::_Acquire_lock(void * _PLockingNode, bool _FHasExternalNode)
{
LockQueueNode * pNewWriter = reinterpret_cast(_PLockingNode);
LockQueueNode * pActiveWriter = reinterpret_cast(_M_activeWriter);
// Locks are non-reentrant, so throw if this condition is detected.
if (pNewWriter->m_pContext == reinterpret_cast(pActiveWriter)->m_pContext)
{
throw improper_lock("Lock already taken");
}
LockQueueNode * pPreviousWriter = reinterpret_cast(InterlockedExchangePointer(&_M_pWriterTail, pNewWriter));
bool doNeedBlock = true;
if (pPreviousWriter == NULL)
{
pNewWriter->UpdateQueuePosition(pActiveWriter);
// This is the only write that currently exists
if (_Set_next_writer(pNewWriter))
{
doNeedBlock = false;
pNewWriter->UnblockWithoutContext();
}
}
else
{
pNewWriter->UpdateQueuePosition(pPreviousWriter);
pPreviousWriter->m_pNextNode = pNewWriter;
// Note: pPreviousWriter is *unsafe* after the assignment above!
}
// Don't block if the context unblocked itself already
if (doNeedBlock)
{
pNewWriter->Block(pActiveWriter->m_ticketState);
// Do another position estimation in case we missed the previous number due to race
pNewWriter->UpdateQueuePosition(pActiveWriter);
}
// Since calls with external nodes will not call _Switch_to_active, make
// sure that we are setting the head and the active node properly.
if (_FHasExternalNode)
{
pActiveWriter->Copy(pNewWriter);
// NOTE: The write to _M_pWriterHead below could be re-ordered on ARM64 with the writes within the ->Copy() above (when Copy is inlined).
// However, this is not an issue because _M_pWriterHead is not concurrently read when the lock has already been acquired by the writer
// (which is the case here). The read of _M_pWriterHead in _Unlock_reader can occur only when a writer is blocked or about to block.
_M_pWriterHead = pNewWriter;
}
}
///
/// The writer node allocated on the stack never really owns the lock. The reason for that is that
/// it would go out of scope and its insides would not be visible in unlock() where it would potentially
/// need to unblock the next writer in the queue. Instead, its state is transferred to the internal
/// writer node which is used as a scratch node.
///
///
/// The writer that needs to own the lock.
///
void reader_writer_lock::_Switch_to_active(void * _PWriter)
{
_CONCRT_ASSERT((_M_lockState & RWLockWriterExclusive) != 0);
LockQueueNode * pWriter = reinterpret_cast(_PWriter);
LockQueueNode * pActiveWriter = reinterpret_cast(_M_activeWriter);
//
// Copy the contents of the writer allocated on the stack which now owns the lock, so that we would
// have its information available during unlock.
//
pActiveWriter->Copy(pWriter);
//
// If someone is adding a writer then wait until next node pointer is populated. Otherwise, there will be no way
// to unblock the next writer after newWriterNode goes out of scope.
//
if (pActiveWriter->m_pNextNode == NULL)
{
//
// If the compare-and-swap to active writer succeeds that means that a new writer coming in will call _Set_next_writer, which
// will properly set the _M_pWriterHead. Otherwise, it has to be set manually when next node is done.
//
if (reinterpret_cast(InterlockedCompareExchangePointer(&_M_pWriterTail, pActiveWriter, pWriter)) != pWriter)
{
pWriter->WaitForNextNode();
//
// During the initial copy the next pointer was not copied over and it has been populated in the meantime.
// This copy can now be safely performed because tail has moved, so next will point to the second element.
//
pActiveWriter->Copy(pWriter);
}
}
_CONCRT_ASSERT(_PWriter != _M_pWriterTail);
// NOTE: The write to _M_pWriterHead below could be re-ordered on ARM64 with the writes within the ->Copy() above (when Copy is inlined).
// However, this is not an issue because _M_pWriterHead is not concurrently read when the lock has already been acquired by the writer
// (which is the case here). The read of _M_pWriterHead in _Unlock_reader can occur only when a writer is blocked or about to block.
_M_pWriterHead = pActiveWriter;
}
///
/// Called from unlock() when a reader is holding the lock. Reader count is decremented and if this
/// is the last reader it checks whether there are interested writers that need to be unblocked.
///
void reader_writer_lock::_Unlock_reader()
{
long resultState = InterlockedExchangeAdd(&_M_lockState, -RWLockReaderCountIncrement);
//
// If this is the last reader and there are writers lined up then unblock them. However,
// if exclusive writer flag is not set, then writers will take care of themselves.
//
if ((resultState & (~RWLockReaderInterested)) == (RWLockReaderCountIncrement | RWLockWriterInterested | RWLockWriterExclusive))
{
_CONCRT_ASSERT(_M_pWriterTail != NULL);
reinterpret_cast(_M_pWriterHead)->Unblock();
}
}
///
/// Constructs a holder object and acquires the reader_writer_lock passed to it.
// If the reader_writer_lock is held by another thread this call will block.
///
///
/// Reader writer to lock.
///
reader_writer_lock::scoped_lock::scoped_lock(reader_writer_lock& _Reader_writer_lock) : _M_reader_writer_lock(_Reader_writer_lock)
{
static_assert(sizeof(LockQueueNode) <= sizeof(_M_writerNode), "_M_writerNode buffer too small");
LockQueueNode * pNewWriterNode = reinterpret_cast(_M_writerNode);
new(pNewWriterNode) LockQueueNode;
_M_reader_writer_lock._Acquire_lock(pNewWriterNode, true);
}
///
/// Destructs a holder object and releases the reader_writer_lock.
///
reader_writer_lock::scoped_lock::~scoped_lock()
{
_M_reader_writer_lock.unlock();
}
///
/// Constructs a holder object and acquires the reader_writer_lock passed to it.
// If the reader_writer_lock is held by another thread this call will block.
///
///
/// Reader Writer to lock.
///
reader_writer_lock::scoped_lock_read::scoped_lock_read(reader_writer_lock& _Reader_writer_lock) : _M_reader_writer_lock(_Reader_writer_lock)
{
_M_reader_writer_lock.lock_read();
}
///
/// Destructs a holder object and releases the reader_writer_lock.
///
reader_writer_lock::scoped_lock_read::~scoped_lock_read()
{
_M_reader_writer_lock.unlock();
}
} // namespace Concurrency