Click here to Skip to main content
Click here to Skip to main content

A .NET-like ReaderWriterLock Class In Native C++

By , 19 Oct 2007
Rate this:
Please Sign up or sign in to vote.

Introduction

The Reader-Writer-Lock mechanism is used to synchronize access to a resource. At any given time, it allows either concurrent read access for multiple threads, or write access for a single thread. In a situation where a resource is changed infrequently, Reader-Writer-Lock provides better throughput than a simple one-at-a-time lock, such as CriticalSection or Mutex.

Background

The System.Threading.ReaderWriterLock class in the .NET Framework provides a new synchronization method that Win32 doesn't. If you have never heard about the concept of Reader-Writer locks, then the .NET document is a good place for you. Following is my summary on the features of the ReaderWriterLock implemented in the .NET Framework:

  1. ReaderWriterLock is used to synchronize access to a resource. At any given time, it allows either concurrent read access for multiple threads, or write access for a single thread.
  2. The class works best where most accesses are reads, while writes are infrequent and of short duration. In such situations, a ReaderWriterLock provides better throughput than a simple one-at-a-time lock, such as CriticalSection or Mutex.
  3. While a writer is waiting for active reader locks to be released, threads requesting new reader locks will have to wait in the reader queue. Their requests are not granted, even though they could share concurrent access with existing reader-lock holders; this helps protect writers against indefinite blockage by readers.
  4. Supports reentrance (allows call to AcquireReaderLock/AcquireWriterLock multiple times in the same thread) and lock escalation (UpgradeToWriterLock when being a reader).

The C++ Reader-Writer-Lock Classes

My implementation of the Reader-Writer-Lock mechanism has all the characteristics of the .NET class except "lock cookie". Moreover, there are two classes in this implementation: each of them has its own convenience (and inconvenience of course). Following are the brief descriptions of their considerable methods:

CReaderWriterLockNonReentrance

AcquireReaderLock Acquires the reader lock. When calling this method, be sure that the calling thread has not owned any lock (reader or writer) yet; otherwise deadlock might happen.
ReleaseReaderLock Releases the reader lock. When calling this method, be sure that the calling thread has already owned the reader lock; otherwise the object's behavior is undefined.
AcquireWriterLock Acquires the writer lock. When calling this method, be sure that the calling thread has not owned any lock (reader or writer) yet; otherwise deadlock might happen.
ReleaseWriterLock Releases the writer lock. When calling this method, be sure that the calling thread has already owned the writer lock; otherwise the object's behavior is undefined.
DowngradeFromWriterLock Releases the writer lock and acquires reader lock in an atomic operation. When calling this method, be sure that the calling thread has already owned the writer lock; otherwise the object's behavior is undefined.
UpgradeToWriterLock

Releases the reader lock and acquires the writer lock. If "timeout" occurs, this method will automatically re-acquire the reader lock before it returns. In other words: if "upgrade" is failed due to "timeout", the calling thread still holds (owns) the reader lock. When calling this method, be sure that the thread has already owned the reader lock; otherwise the object's behavior is undefined.
IMPORTANT: If "timeout" != 0, other threads might write to the resource before this method returns regardless of whether the calling thread is upgraded successfully or not.

The demo project (attached with this article) also shows a non-intended behavior of the CReaderWriterLockNonReentrance class (call AcquireXXX in a thread then call ReleaseXXX in another thread). Although the non-intended feature is not recommended, it is very useful in some situations; let's use it with care.

CReaderWriterLock

AcquireReaderLock A thread can call AcquireReaderLock multiple times, which increments the reader lock counter each time. You must call ReleaseReaderLock once for each time you call AcquireReaderLock. Alternatively, you can call ReleaseAllLocks to reduce the lock count to zero immediately.
NOTE: A reader lock request is always granted immediately when one of the following two conditions is satisfied:
a) Current thread already owned the writer lock; this is to prevent a thread from blocking on itself.
b) Current thread already owned the reader lock (support recursive lock)
ReleaseReaderLock Decrements the reader lock counter. When the counter reaches zero, the lock is released. When calling this method, be sure that the calling thread already owned the reader lock; otherwise exception will be raised in DEBUG mode.
AcquireWriterLock

A thread can call AcquireWriterLock multiple times, which increments the writer lock counter each time. You must call ReleaseWriterLock once for each time you call AcquireWriterLock. Alternatively, you can call ReleaseAllLocks to reduce the lock count to zero immediately.
NOTE:
a) To prevent a thread from blocking on itself, a writer lock request is always granted immediately when current thread already owned the writer lock.
b) If calling thread already owned the reader but not the writer lock, it will be "upgraded" to own the writer lock implicitly.
IMPORTANT: If an implicit "upgrade" was made and "timeout" != 0, other threads might write to the resource before this method returns regardless whether the calling thread is upgraded successfully or not.

ReleaseWriterLock Decrements the writer lock counter. When the counter reaches zero, the lock is released. When calling this method, be sure that the calling thread already owned the writer lock; otherwise exception will be raised in DEBUG mode.
NOTE: If the object detects that this request is corresponding to a previous auto-upgrade, it will also downgrade automatically (releases writer lock but still keep reader lock).
ReleaseAllLocks Resets all lock counters (writer and reader counters) of calling thread to zero and releases both the writer & the reader lock regardless of how many times the thread owned reader or writer locks.
NOTE: After which, any call to ReleaseWriterLock or ReleaseReaderLock will raise an exception in DEBUG mode.
GetCurrentThreadStatus Retrieves lock counters (both reader & writer counters) of calling thread.

As someone may notice, there is neither "upgrade" nor "downgrade" method as in the CReaderWriterLockNonReentrance class. Actually, we don't need these methods since these actions ("upgrade" or "downgrade") will be done implicitly and automatically.

Some Helper Classes

CAutoReadLock(T) and CAutoWriteLock(T) would let a thread acquire a lock in a body of code, and not have to worry about explicitly releasing that lock if an exception is encountered in that piece of code or if there are multiple return points out of that piece.

Internal Implementation & Efficiency

In this implementation, each Reader-Writer-Lock object consists of three synchronization objects:

  1. A CriticalSection object. This object is needed to guard all the other members so that manipulating them can be accomplished atomically. Internally, a CriticalSection object is a block of memory (24 bytes) plus a kernel event object (use the CreateEvent function). See "Break Free of Code Deadlocks in Critical Sections Under Windows" to understand why I said that. The event object is dynamically created on demand if there is more than one thread waiting on a CriticalSection object. If a critical section code is very small (enters and then leaves in a very short time), the event object will almost never be created. That explains why a CriticalSection is the fastest synchronization method in Win32. Taking a look at the source code, you would see that my implementation satisfies this condition – the code inside each critical section is very small/short. As a result, the event object will almost never be created.
  2. A manual–reset event object. This object is dynamically created when new readers have to wait until there is no writer on the Reader-Writer-Lock object. This event object will be automatically deleted to save system resources when no longer needed. This event object plays the role of what the .NET team calls a "reader queue."
  3. An auto-reset event object. This object is dynamically created when new writers have to wait until the Reader-Writer-Lock object is not locked by any active thread (readers or a writer). It will also be automatically deleted to save system resources when no longer needed. This event object plays the role of what the .NET team calls a "writer queue."

To summarise, when using the classes in a right way (multiple readers, almost no writer), a Reader-Writer-Lock object almost doesn't use any kernel object, and reader threads almost never do WaitForXXX operations. This algorithm determines the efficiency of my implementation: fast, and consumes very little kernel resource. "Consume very little kernel resource" will be a great feature in case you need many Reader-Writer-Lock objects in your application at the same time.

Discussion

In comparison to the CReaderWriterLockNonReentrance class, the CReaderWriterLock one is easier to use and end-users (developers) don't have to worry about deadlock as with the non-reentrance version. However the non-reentrance version is faster and more efficient in memory usage: it doesn't have a hash-table to maintain a counter for each thread so it avoids the overhead of dynamic memory allocation. Naturally, a common question will occur in our mind: which one should we use? It's not easy to answer this question and what follows are just my own thoughts:

  1. If your team members (colleagues) didn't spend enough time with multi-threading & synchronization then CriticalSection & Mutex is a good choice to reduce the risk of wrong usage.
  2. If concurrence and high-speech are essential, an experienced person would prefer the non-reentrance version (CReaderWriterLockNonReentrance) because he/she could control everything.
  3. If concurrence and high-speech are essential but reentrance problem is very hard to avoid, the CReaderWriterLock one seems to be a must. In this case, consider replacing the STL map class by a more appropriate one to reduce overhead of dynamic memory allocation. For your reference, I would like to introduce the article "A Custom Block Allocator for Speeding Up VC++ STL" by Joaquín M López Muñoz which could be integrated into this library just in few minutes, but I left this task for you to keep my source code simple.
  4. If you don't want to waste a CriticalSection object, consider using Spin Lock to replace it.

History

  • Oct-17-2007: Version 2.0
    • Timeout supported
    • In the class CReaderWriterLockNonReentrance, renamed the method ReleaseReaderAndAcquireWriterLock to UpgradeToWriterLock
  • Jan-14-2007: Version 1.2
    • Revised some methods to make code inside the Critical Section to be as short as possible
    • Added more methods into CReaderWriterLock class for convenience
    • Last but most important, CReaderWriterLockNonReentrance has been revised deeply to make it more usable in real work than in the previous version
  • Feb-28-2006: Version 1.1
    • Changed the license to "Lesser General Public License" (thanks to Mitchel Haas)
    • Added support for non-MFC projects by re-defining the ASSERT and VERIFY macros
    • Added some helper classes, CAutoReadLock and CAutoWriteLock (thanks to Andy318)
    • Modified the demo project to show a non-intended behavior of the CReaderWriterLockNonReentrance class
  • Feb-01-2006: Version 1.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Quynh Nguyen
Global Cybersoft (Vietnam)
Vietnam Vietnam
Quynh Nguyen is a Vietnamese who has worked for 7 years in Software Outsourcing area. Currently, he works for Global Cybersoft (Vietnam) Ltd. as a Project Manager in Factory Automation division.
 
In the first day learning C language in university, he had soon switched to Assembly language because he was not able to understand why people cannot get address of a constant as with a variable. With that stupid starting, he had spent a lot of his time with Assembly language during the time he was in university.
 
Now he is interesting in Software Development Process, Software Architecture and Design Pattern… He especially indulges in highly concurrent software.

Comments and Discussions

 
QuestionWhy not use boost to create read-write lock PinmemberLior Kogan24-Jun-11 23:27 
AnswerRe: Why not use boost to create read-write lock Pinmemberelbertlev1-Dec-11 5:56 
GeneralLicense PinmemberPablo Yabo20-May-11 5:18 
GeneralWarning, there is a deadlock when using UpgradeToWriter Pinmemberxryl66927-Jun-09 0:06 
GeneralDoesn't work (serious issue) Pinmemberxryl66922-Jun-09 21:27 
AnswerRe: Doesn't work (serious issue) [modified] PinmemberSamNorris20-Jul-10 19:00 
Here are some necessary changes that fix this problem.
 
There are three places that need the scoping lock moved down. Search for this sequence:
 
      m_impl.LeaveCS();
      ite->second += ...;
 
those three places need the call to LeaveCS moved to below the addition:
 
      ite->second += ...;
      m_impl.LeaveCS();
 
Otherwise there is a risk that other threads have altered the m_map member and (due to reallocs or insertions above the current iterator) caused the iterator value (ite) to be referencing the wrong thread's data.
 

Similarly, there's a gap when upgrading from a reader to writer. Here's the problem code in CReaderWriterLock::AcquireWriterLock(...) :
 
         // Try upgrading from reader to writer
         blCanWrite = m_impl._UpgradeToWriterLockAndLeaveCS(dwTimeout);
         if(blCanWrite)
         {
               ite->second += WRITER_RECURRENCE_UNIT;
         }
 
This iterator can't be used here without first searching again (within a CS) as the m_map may have been altered again since leaving the CS. The corrected code should look like this:
 
         // Try upgrading from reader to writer
         blCanWrite = m_impl._UpgradeToWriterLockAndLeaveCS(dwTimeout);
         if(blCanWrite)
         {
               // *** There's a gap here - m_impl.m_iNumOfWriter was 1 so should be ok (added asserts to catch any problem)
               m_impl.EnterCS();
               _HASSERT(m_impl.m_iNumOfReaderEntered == 0);
               _HASSERT(m_impl.m_iNumOfWriter >= 1);
               CMapThreadToState::iterator ite = m_map.find(dwCurrentThreadId);
               _HASSERT(ite != m_map.end() && ite->second > 0);
               ite->second += WRITER_RECURRENCE_UNIT;
               m_impl.LeaveCS();
         }
 
I don't think this last problem is as likely to occur as the first three (above) but it is still possible.

modified on Friday, July 23, 2010 6:55 PM

Questionask a question PinmemberMember 243138818-May-09 4:22 
QuestionTimeout support PinmemberNicolas Bonamy21-May-07 23:31 
AnswerRe: Timeout support PinmemberQuynh Nguyen24-May-07 3:02 
GeneralRe: Timeout support PinmemberNicolas Bonamy24-May-07 23:15 
GeneralRe: Timeout support PinmemberQuynh Nguyen25-May-07 6:30 
GeneralRe: Timeout support PinmemberNicolas Bonamy25-May-07 21:52 
GeneralRe: Timeout support PinmemberQuynh Nguyen25-May-07 22:10 
NewsTimeout is now supported (message from author) PinmemberQuynh Nguyen17-Oct-07 18:43 
GeneralA Few thing i wud like to know Pinmemberkapil bhavsar1-Apr-07 22:39 
GeneralRe: A Few thing i wud like to know PinmemberQuynh Nguyen2-Apr-07 11:17 
QuestionQuestions Pinmemberncpga19-Jan-07 11:48 
AnswerRe: Questions PinmemberQuynh Nguyen19-Jan-07 17:24 
GeneralRe: Questions Pinmemberncpga19-Jan-07 19:10 
GeneralRe: Questions PinmemberQuynh Nguyen19-Jan-07 19:16 
GeneralReal nice work Pinmemberzli9829-Sep-06 10:34 
GeneralRe: Real nice work PinmemberQuynh Nguyen3-Oct-06 23:54 
GeneralRe: Real nice work Pinmembermmatitya17-Jan-07 21:49 
GeneralBy the way, the article linked to has a serious error too PinmemberJoelKatz28-Sep-06 23:03 
GeneralRe: By the way, the article linked to has a serious error too PinmemberQuynh Nguyen29-Sep-06 6:56 
GeneralRe: By the way, the article linked to has a serious error too Pinmemberpeterchen6-Feb-07 11:27 
GeneralWow, serious bug PinmemberJoelKatz28-Sep-06 22:56 
GeneralRe: Wow, serious bug PinmemberQuynh Nguyen29-Sep-06 6:19 
GeneralNice ! Pinmemberandy31826-Feb-06 18:00 
GeneralRe: Nice ! PinmemberQuynh Nguyen27-Feb-06 2:52 
GeneralRe: Nice ! PinmemberQuynh Nguyen28-Feb-06 2:50 
GeneralRe: Nice ! Pinmembersweetmelon13-Mar-06 20:50 
GeneralRe: Nice ! PinmemberQuynh Nguyen14-Mar-06 3:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140421.2 | Last Updated 19 Oct 2007
Article Copyright 2006 by Quynh Nguyen
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid