Recursive, Upgradable Read/Write Lock for Windows

Environment: VC6, Windows NT/2000; should work on 98/Me, but not tested there

Read/write lock is a synchronization mechanism that protects a shared resource in a multithreaded environment. The standard mutex (and critical section) object provided by Windows allows only one thread a time to access the resource. In many cases, this is overkill. Let's assume we have two or more reader threads that read the contents of the resource without changing it, and one or more writer threads that can both read and write to the resource. In this case, we can relax the rules of protection a little bit:

  • A reader can access the resource when no writers are active.
  • A writer can access the resource when no other reader or writer is active.
  • Several readers can access the resource concurrently.

Many operating systems implement this kind of protection natively. Unfortunately, no native synchronization object provided by Windows offers such a functionality. Nevertheless, read/write locks can be built in Windows, based on available standard synchronization objects.

This article offers two lock objects:

  • KReadWriteLock—simple read/write lock, and
  • KReadWriteLockEx—extended read/write lock.

They have almost identical interfaces, but slightly different implementation.

KReadWriteLock Class

class KReadWriteLock
{
  KReadWriteLock( KReadWriteLock const& ); // not implemented
  void operator=( KReadWriteLock const& ); // not implemented

public:
  KReadWriteLock();

  bool LockRead(    DWORD Timeout = INFINITE );
  bool LockWrite(   DWORD Timeout = INFINITE );
  bool UnlockRead(  DWORD Timeout = INFINITE );
  bool UnlockWrite( DWORD Timeout = INFINITE );
};

Locking rules for KReadWriteLock are as follows:

  • Any number of readers can acquire the mutex for read (LockRead()).
  • Only a single writer can acquire the mutex for write (LockWrite()).
  • No other writers or readers can hold the mutex while the writer uses it.

KReadWriteLock prefers neither writers nor readers. In other words, if both writer(s) and reader(s) are waiting for the lock, any of them can grab the lock when it is released by the previous owner.

KReadWriteLock is recursive, but not upgradable. This means that:

  • If you have a read lock, you can safely acquire another read lock.
  • If you have a read lock, you CANNOT acquire a write lock (deadlock!).
  • If you have a write lock, you can safely acquire another write lock.
  • If you have a write lock, you CANNOT acquire a read lock (deadlock!).

KReadWriteLockEx Class

class KReadWriteLockEx
{
  ReadWriteLockEx( KReadWriteLockEx const& ); // not implemented
  void operator=( KReadWriteLockEx const& );  // not implemented

public:
  KReadWriteLockEx();

  bool LockRead(    DWORD Timeout = INFINITE );
  bool LockWrite(   DWORD Timeout = INFINITE );
  bool UnlockRead(  DWORD Timeout = INFINITE );
  bool UnlockWrite( DWORD Timeout = INFINITE );
};

KReadWriteLockEx offers more flexibility: it is both recursive and upgradable. That is, if you have a read lock, you can safely acquire a write lock and vice versa. Furthermore, you can even overlap locks:

KReadWriteLockEx Lock;
Lock.LockRead();
Lock.LockWrite();
Lock.UnlockRead();
Lock.UnlockWrite();

KReadWriteLockEx prefers writers over readers. That is, if a writer is waiting on the lock, no new readers are allowed to access the resource. Existing readers can continue to use the resource until they release the lock. This prevents so-called "writer starvation".

For all this flexibility, KReadWriteLockEx is slower than KreadWriteLock, takes more memory, and is more complex in its implementation. This is because KReadWriteLockEx must manually keep track of what threads currently own the lock. In the case of KReadWriteLock, this job is done by the operating system.

Note: neither KReadWriteLock nor KReadWriteLockEx will work between processes. However, they can be extended to do so.

Helper Classes

Helper classes KReadLock, KWriteLock, KReadLockEx, and KWriteLockEx are used for automatic acquisition and release of locks. They are similar in nature to the CSingleLock class from MFC. Their interfaces are almost identical and look like this:

class KReadLock
{
    KReadLock(KReadWriteLock& Lock, bool bInitialLock = true);
    ~KReadLockTempl();

    bool Lock(  DWORD Timeout = INFINITE);
    bool Unlock(DWORD Timeout = INFINITE);
};

Use of methods is pretty straightforward and similar to CSingleLock. Note, that unlike CSingleLock, helper classes will acquire the lock in constructor by default.

Downloads

Download demo project - 26 Kb
Another demo - using helper classes - 23 Kb
Download source - 4 Kb


Comments

  • Question about upgradable feature

    Posted by Panisset on 11/15/2005 07:47pm

    After I study your code I found a problem when you said that reading threads must acquire the write lock when they request because there are no other writers. What about other reading thread that could be as writer right now ?

    • Code changes

      Posted by Nevyn_IT on 11/24/2005 07:07pm

      It is the concept that is wrong. In fact if you just in the test app create a few readers and then quickly make them all wait for write lock, they ALL get the lock!!!!! The concept is that if there is a waiting writer, you simply cannot upgrade the read lock, so the code has to be changed:

      bool KReadWriteLockEx::LockWrite( DWORD Timeout )
      [omissis]
      
      if (!Info.IsWriter())
      {
        ASSERT(Info.IsReader());
      
        //If someone else is waiting to write, and we wait without releasing our read lock,
        //it can end up in a deadlock. So the simplest strategy is to fail immediately
        if (m_TotalWriterThreads != 0)
        {
          ReleaseMutex(m_Access);
          return false;
        }
      
      // Since we are the only active reader, there can be no other writers
      // Grab writer mutex - it MUST be available
      VERIFY( Wait1(m_WriterMutex, 0 ) );
      
      [omissis]
      
      And it works, doing the best it can, granting the possibility of waiting only at the first that waits.

      Reply
    Reply
  • Works in ATL too with a bit of modification

    Posted by sjorden on 09/14/2004 01:56am

    Just #include <atlsync.h> HOWEVER ATL doesn't seem to create the mutex properly with the void constructor in CMutex so you will have to make the KReadWriteLockEx constructor look like this: KReadWriteLockEx::KReadWriteLockEx() : m_CanRead(true, true), // initially signalled,manual reset m_CanWrite(true, true), // ditto m_Access(false), m_WriterMutex(false), m_TotalReaderThreads(0), m_TotalWriterThreads(0) { {; Alternatively, since the CMutex/CEvent classes aren't doing much anyways, you could replace them with primitive HANDLEs and CreateMutex, CreateEvent, etc.

    • umm nice input formatting..

      Posted by sjorden on 09/14/2004 01:59am

      Just #include <atlsync.h> HOWEVER
      
      ATL doesn't seem to create the mutex properly 
      with the void constructor in CMutex so you will have to make
      the KReadWriteLockEx constructor look like this: 
      
      KReadWriteLockEx::KReadWriteLockEx()
      :
      m_CanRead(true, true),  // initially signalled,manual reset
      m_CanWrite(true, true), // ditto
      m_Access(false),
      m_WriterMutex(false),
      m_TotalReaderThreads(0),
      m_TotalWriterThreads(0)
      {
      {;
      
      Alternatively, since the CMutex/CEvent classes 
      aren't doing much anyways, you could replace them 
      with primitive HANDLEs and CreateMutex, 
      CreateEvent, etc.

      Reply
    Reply
  • Works in ATL too with a bit of modification

    Posted by sjorden on 09/14/2004 01:55am

    Just #include  HOWEVER
    
    ATL doesn't seem to create the mutex properly with the void constructor in CMutex so you will have to make the KReadWriteLockEx constructor look like this: 
    
    KReadWriteLockEx::KReadWriteLockEx()
    :
    m_CanRead(true, true),  // initially signalled,manual reset
    m_CanWrite(true, true), // ditto
    m_Access(false),
    m_WriterMutex(false),
    m_TotalReaderThreads(0),
    m_TotalWriterThreads(0)
    {
    {;
    
    Alternatively, since the CMutex/CEvent classes aren't doing much anyways, you could replace them with primitive HANDLEs and CreateMutex, CreateEvent, etc.

    Reply
  • Codeguru-Visual C++ or C# or .NET

    Posted by Legacy on 02/19/2003 12:00am

    Originally posted by: Duong Dai Thang

    I'm  Thang, now I have been learning progrmming, through address of codeguru.com, I see your pages post some lesson very excelent. So, I want to join your club, please mail to me as well as you receive my mail.
    
    Thank! Your Truthful!

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • With 81% of employees using their phones at work, companies have stopped asking: "Is corporate data leaking from personal devices?" and started asking: "How do we effectively prevent corporate data from leaking from personal devices?" The answer has not been simple. ZixOne raises the bar on BYOD security by not allowing email data to reside on the device. In addition, Zix allows employees to maintain complete control of their personal device, therefore satisfying privacy demands of valued employees and the …

  • Available On-Demand Today's changing workforce dynamics, economic challenges, and technological advances are placing immense pressure on business leaders to turn their focus on people – their most valuable asset – in order to remain competitive. Research shows that a significant number of new employees quit within one year of taking a new job*. Whether it's through a merger and acquisition, or standard hiring process, like any first impression, early experiences shape their opinions of their new …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds