CXReadWriteLock

From cxwiki

The CXReadWriteLock class provides a readers-writer (shared-exclusive) lock mechanism which is otherwise similar to CXRecursiveMutex. The object exists in one of two states: LOCKED or UNLOCKED, with the LOCKED state maintaining a reference count and a share type.
 
The share type is specified when attempting to take the lock, with the following share types supported:
  • EXCLUSIVE - Blocks while there are outstanding locks. Does not block when called recursively.
  • SHARED_LONG - Blocks while there are outstanding EXLUSIVE locks or lock attempts. May be granted to multiple threads concurrently. Does not block when called recursively.
  • SHARED_SHORT - Blocks while EXCLUSIVE locks are currently held. Blocks if there are outstanding EXCLUSIVE lock attempts unless at least one SHARED_LONG lock is currently held. May be granted to multiple threads concurrently. Does not block when called recursively.
 
The unlocks must be performed by the lock-owner thread, and must specify the same share type as the lock. When multiple locks have been taken recursively, the unlocks must be performed in exact reverse order. A recursive lock attempt is permitted to use a less exclusive or equal share level (SHARED_SHORT <= SHARED_LONG <= EXCLUSIVE), but may not attempt to use a more exclusive share level.
 
enum ShareTypeEnum
{
  SHARED_SHORT = 2,
  SHARED_LONG = 3,
  EXCLUSIVE = 4,
};

// Lock the mutex to the current thread. Blocks while any other thread still has the mutex locked.
void LockMutex(ShareTypeEnum shareType) const; 

// Unlocks the mutex from the current thread. Must be called once per call to LockMutex().
void UnlockMutex(ShareTypeEnum shareType) const;

// Attempts to lock the mutex, but does not block. Returns true if a lock was successfully taken.
bool TryAndLockMutex(ShareTypeEnum shareType) const;

Restrictions

  • The CXReadWriteLock is UNLOCKED when constructed.
  • The CXReadWriteLock should be returned to the UNLOCKED state prior to destruction. It is considered an error for the application to destroy a CXReadWriteLock while it is locked, because that would likely to lead to unlock attempts after destruction.
  • An attempt to lock or unlock a CXReadWriteLock during or after its destruction is considered an error. The resultant behaviour is undefined. This includes the scenario where a lock attempt is placed prior to destruction, but cannot be honored until destruction has begun.
  • No attempt is made to detect or recover from deadlocks. It is the callers responsibility to ensure that locks are taken and released in an approriate manner to avoid deadlocks.
  • It is considered an error to attempt to unlock a mutex on a thread which is not the lock-owner.
  • It is considered an error to attempt to lock the mutex recursively with a more-exclusive share type than the original lock. The resultant behaviour is undefined. TryAndLockMutex() is defined to return false in this scenario; calling it is not an error.
  • It is considered an error to attempt to unlock a mutex with a different shareType to which it was locked. When undertaking recursive locks, the mutex must be unlocked in LIFO order.

Performance

CXReadWriteLock may implement substantial logic on top of the OS locking primitives. It is expected to be slower than a CXRecursiveMutex. It should be used in scenarios where the benefits of multiple threads sharing the mutex in the common case outweighs both the extra costs of the lock itself, and the costs of losing sharing support during an EXCLUSIVE lock operation.
 
CXReadWriteLock inherits the underlying OS guarantees for mutex fairness between lock attempts using the EXCLUSIVE share type. Other share types offer no fairness guarantees.
 
A typical read-write implementation offers only two share type: "SHARED" and "EXCLUSIVE". The distinction between SHARED_SHORT and SHARED_LONG was added to deal with use-cases where long-running SHARED operations were used alongside long-running EXLUSIVE operations and also very-short-running SHARED operations. In this scenario, the very-short-duration operations are typically forced to wait until any queued EXLUSIVE operations complete, which in turn must wait for any outstanding SHARED operations. As a result, you have the scenario that a short-run-time SHARED operation is suspended, awaiting a long-run-time SHARED operation. This is obviously non-sensical but is required to ensure that the EXCLUSIVE lock can eventually be granted. Adding the third share type allows us to avoid this scenario with a best-case outcome of allowing a lot of extra SHARED_SHORT operations to complete while the EXCLUSIVE operations are waiting for the SHARED_LONG operations. The worst case scenario is that a SHARED_SHORT operation begins and the SHARED_LONG operation then finishes, causing the EXCLUSIVE operation to be further delayed until the SHARED_SHORT operation also completes. It is expected that SHARED_SHORT operations should be trivial enough that such a delay is not impactful.