The article shows how you can create a simple synchronization primitive that restricts access to a shared resource to a single writer, while still allowing multiple readers to access it simultaneously. Of course, it's not supposed to replace the highly optimized default ReaderWriterLockSlim implementation, but nonetheless, it's a very interesting demonstration of how Monitor.Wait and Monitor.PulseAll can be used to achieve the goal.
Introduction
The standard Monitor.Wait
and Monitor.PulseAll
methods are rarely used today, but still could provide powerful capabilities for thread synchronization. As mentioned, we're going to implement a standard Reader-Writer lock, so let's dive into the code straight away:
private readonly object syncRoot = new object();
private readonly object syncWrite = new object();
private int rcount = 0;
private int wcount = 0;
That's right, all we need is two counters - one for each owner type and two root objects for synchronization. Next, let's explore the EnterXXXLock sections:
public void EnterReadLock()
{
lock (syncRoot)
{
while (wcount > 0)
{
Monitor.Wait(syncRoot);
}
rcount++;
}
}
public void EnterWriteLock()
{
lock (syncRoot)
{
wcount++;
while (rcount > 0)
{
Monitor.Wait(syncRoot);
}
}
Monitor.Enter(syncWrite);
}
The EnterReadLock
method allows the reader to continue only when the resource is not being accessed by writers. The EnterWriteLock
method, in turn, immediately notifies the presence of the writer, and then waits until readers release the lock. Technically, this code splits incoming requests into two large groups of writers and readers, with the former taking precedence. Therefore, we need an extra call to Monitor.Enter(syncWrite)
to ensure that only one writer can access at a given time. The following release logic:
public void ExitReadLock()
{
lock (syncRoot)
{
if (--rcount == 0 && wcount > 0)
{
Monitor.PulseAll(syncRoot);
}
}
}
public void ExitWriteLock()
{
Monitor.Exit(syncWrite);
lock (syncRoot)
{
if (--wcount == 0)
{
Monitor.PulseAll(syncRoot);
}
}
}
The last reader or writer in a row just wakes up all waiting threads so they can continue the race. To demonstrate how this works, I've created a console application that runs multiple threads accessing the same resource at the same time. That's the sample output generated by the app:
The source code is available for download at this link. Just un-zip it and execute the dotnet run
command.
History
- 23rd January, 2022: Initial version