ReaderWriterLock Class in C# Threading


Introduction

The .NET Framework provides several threading locking primitives. The ReaderWriterLock is one of them.

The ReaderWriterLock class is used to synchronize access to a resource. At any given time, it allows concurrent read access to multiple (essentially unlimited) threads, or it allows write access for a single thread. In situations where a resource is read frequently but updated infrequently, a ReaderWriterLock will provide much better throughput than the exclusive Monitor lock.

Namespace: System.Threading

This namespace provides all the class methods and properties required for implementation of the ReaderWriterLock class. 

Assembly: mscorlib (in mscorlib.dll)

Constructors

ReaderWriterLock: The constructor used to initialize a new instance of the ReaderWriterLock class.

Properties 

  • IsReaderLockHeld: This property gets a value that points to whether the current thread holds a reader lock.
  • IsWriterLockHeld:This property gets a value that points  to whether the current thread holds the writer lock.
  • WriterSeqNum: Gets the current sequence number.

Method

  • AcquireReaderLock(Int32): Acquires a reader lock, using an Int32 value for the time-out.
  • AcquireWriterLock(Int32): Acquires the writer lock, using an Int32 value for the time-out.
  • ReleaseLock: Releases the lock, regardless of the number of times the thread acquired the lock.
  • ReleaseReaderLock: Decrements the lock count.
  • ReleaseWriterLock: Decrements the lock count on the writer lock.
  • UpgradeToWriterLock(Int32): Upgrades a reader lock to the writer lock, using an Int32 value for the time-out.
  • RestoreLock: Restores the lock status of the thread to what it was before calling ReleaseLock.

The problem with ReaderWriterLock is with its implementation. Several experts have slammed this technique and found that outside of limited scenarios, it is actually far slower than the Monitor.Enter method used to get an exclusive lock. ReaderWriterLock gives higher priority to reader threads then writers. This makes sense if you have many readers and only a few writers. So a lot of readers are able to read the resource while the writer has to wait longer to get the lock. But what If you have equal or more writers. The process of favoring readers make writer threads queue up and take a very long time to complete.

ReaderWriterLock intelligently handles recursive lock requests from the same thread. That is, if the thread calls AcquireReaderLock recursively (i.e., it already holds a reader lock), the lock is granted immediately and the lock count is increased. The thread must still call ReleaseReaderLock as many times as it called AcquireReaderLock. Or, it can call ReleaseLock to reduce the lock count to zero immediately. Be careful with ReleaseLock, though. If you subsequently call ReleaseWriterLock or ReleaseReaderLock, the runtime will throw an exception.

A thread can hold a reader lock or a writer lock, but not both at the same time. Instead of releasing a reader lock in order to acquire the writer lock, you can use UpgradeToWriterLock and DowngradeFromWriterLock.

In some situations, you might find that you're holding a reader lock and you need to upgrade to a writer lock. In that situation, call UpgradeToWriterLock, but understand that you don't get the writer lock immediately. Your code will have to wait for any other readers in the queue to release their locks, and will also have to wait behind any other writers that are already in the write queue.

A thread should not call AcquireWriterLock while it holds a reader lock. Doing so will cause the thread to block while it holds the reader lock, and will lead to a deadlock if you use an infinite timeout. You can call the IsReaderLockHeld method to determine if your thread currently holds a reader lock before you attempt to acquire a writer lock.

Note that the opposite — calling AcquireReaderLock whild holding a writer lock — is just fine. Since the thread has an exclusive lock on the resource, granting the lock is okay. However, if you need to know whether your thread is currently holding a writer lock, you can call IsWriterLockHeld.

The ReaderWriterLock class supports recursion; due to this it causes performance loss. in this case the class needs to maintain a record of the number of times each thread acquires the lock and increment and decrement the counter. When multiple reader threads acquire the same lock (remember ReaderWriterLock class allows simultaneous reads), a counter is maintained for each thread. This overhead is what causes the ReaderWriterLock to pale in comparison to the Monitor class. It is approximately 6 times slower.

Code

In the following example we have three threads where the Thread 2 is a writer one while the Threads 1 and 3 are reader threads. When you take a look at the output, it's clear that the readers are granted simultaneous access to the initial variable, but when the writer thread is writing to it, all the readers wait in queue patiently for it to finish executing. I have made the DoWorkRead method parametrized in order to identify the thread on which the method is being executed.

using System;
using System.Threading;
namespace ReaderWriterLockClass
{
class akshay
    {
        int initial = 0;
        ReaderWriterLock rwl = new ReaderWriterLock();
      public void myRead(object threadName)
        {
            //Accquire Reader Lock.
            rwl.AcquireReaderLock(Timeout.Infinite);
            Console.WriteLine("Read start: Thread: " + threadName + " " + initial);
            if (threadName.ToString() == "Thread 1")
                //Irregular sleeps makes more chances of
                //Multiple threads trying to access it
                //at same time
                Thread.Sleep(10);
            else
                Thread.Sleep(250);
            Console.WriteLine("Read end  : Thread: " + threadName + " " + initial);
            rwl.ReleaseReaderLock();
            //Release Lock
        }
        public void myWrite()
        {
            rwl.AcquireWriterLock(Timeout.Infinite);
            Console.WriteLine("\nWriter start: " + initial);
            initial++; //Writing
            Console.WriteLine("Writer End: " + initial);
            rwl.ReleaseWriterLock();
            Console.WriteLine();
        }
        static void Main(string[] args)
        {
            akshay p = new akshay();
            for (int i = 0; i < 5; i++)
            {
                Thread t1 = new Thread(p.myRead); //Reader Thread
                //Writer Thread
                Thread t2 = new Thread(new ThreadStart(p.myWrite));
                //Reader Again
                Thread t3 = new Thread(p.myRead);
                //Start all threads
                t1.Start("Thread 1");
                t2.Start();
                t3.Start("Thread 3");
                //Wait for them to finish execution
                t1.Join();
                t2.Join();
                t3.Join();
            }
            Console.Read();
       } 
    }
}

Output

A look at the output shows that once the writer acquires a lock, it is mutually exclusive, but both the reader threads are able to access the variable initially.

Readerwriterlock1.gif

We can specify Timeout.Infinite if you really don't want the lock request to time out. You should be very careful with infinite timeout values, though. Without timeouts, a misbehaving thread that leaves the resource locked will cause a deadlock in your application that is very difficult or impossible to diagnose. At least with timeouts, you can catch the timeout exception and report that the resource is locked.

It's critical that your code release any lock that it obtains. If a thread obtains a reader lock, then it must call ReleaseReaderLock to release the lock. Similarly, any call to AcquireWriterLock must be balanced by a call to ReleaseWriterLock. If you fail to release the locks, your thread will keep the resource locked and other threads will not be able to access it.

One more problem with the ReaderWriterLock class is that it allows Reader threads to acquire writer locks. If you set an infinite timeout, it will create a deadlock situation, where the thread just waits to get the Writer lock but can't because the very same thread holds on to the Reader lock and is yet to release it. So the application just waits and waits.

using System;
using System.Threading;
namespace ReaderWriterLockClass
{
class akshay
    {
        int initial = 0;
        ReaderWriterLock rwl = new ReaderWriterLock();
      public void myRead(object threadName)
        {
            //Accquire Reader Lock.
            rwl.AcquireReaderLock(Timeout.Infinite);
            Console.WriteLine("Read start: Thread: " + threadName + " " + initial);
            //NEVER EVER DO THE BELOW. IT WILL CREATE A DEADLOCK
            rwl.AcquireWriterLock(Timeout.Infinite);
            Thread.Sleep(10);
            Console.WriteLine("Read start: Thread: " + threadName + " " + initial);
            rwl.ReleaseReaderLock();
           
//Release Lock
        }
        public void myWrite()
        {
            rwl.AcquireWriterLock(Timeout.Infinite);
            Console.WriteLine("\nWriter start: " + initial);
            initial++; //Writing
            Console.WriteLine("Writer End: " + initial);
            rwl.ReleaseWriterLock();
            Console.WriteLine();
        }
        static void Main(string[] args)
        {
            akshay p = new akshay();
            for (int i = 0; i < 5; i++)
            {
                Thread t1 = new Thread(p.myRead); //Reader Thread
                //Writer Thread
                Thread t2 = new Thread(new ThreadStart(p.myWrite));
                //Reader Again
                Thread t3 = new Thread(p.myRead);
                //Start all threads
                t1.Start("Thread 1");
                t2.Start();
                t3.Start("Thread 3");
                //Wait for them to finish execution
                t1.Join();
                t2.Join();
                t3.Join();
            }
            Console.Read();
       } 
    }
}

Output

rwl2.gif