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.
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