Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid blocking ReaderWriterLockSlim readers when writer is attempting to enter write lock

I'm using ReaderWriterLockSlim to guard some operations. I would like to favor readers over writers, so that when a reader holds the lock for long and a writer is attempting to acquire the write lock, further readers down the road are not blocked by the writer's attempt (which is what would happen instead if the writer was blocked on lock.EnterWriteLock()).

To this end, I though that the writer could use TryEnterWriteLock with a short timeout in a loop, so that subsequent readers would still be able to acquire the read lock while the writer can't. However, to my surprise, I found out that an unsuccessful call to TryEnterWriteLock changes the state of the lock, blocking future readers anyway. Proof of concept code:

System.Threading.ReaderWriterLockSlim myLock = new System.Threading.ReaderWriterLockSlim(System.Threading.LockRecursionPolicy.NoRecursion);

System.Threading.Thread t1 = new System.Threading.Thread(() =>
{
    Console.WriteLine("T1:{0}: entering read lock...", DateTime.Now);
    myLock.EnterReadLock();
    Console.WriteLine("T1:{0}: ...entered read lock.", DateTime.Now);

    System.Threading.Thread.Sleep(10000);
});

System.Threading.Thread t2 = new System.Threading.Thread(() =>
{
    System.Threading.Thread.Sleep(1000);

    while (true)
    {
        Console.WriteLine("T2:{0}: try-entering write lock...", DateTime.Now);
        bool result = myLock.TryEnterWriteLock(TimeSpan.FromMilliseconds(1500));
        Console.WriteLine("T2:{0}: ...try-entered write lock, result={1}.", DateTime.Now, result);

        if (result)
        {
            // Got it!
            break;
        }

        System.Threading.Thread.Yield();
    }

    System.Threading.Thread.Sleep(9000);
});

System.Threading.Thread t3 = new System.Threading.Thread(() =>
{
    System.Threading.Thread.Sleep(2000);

    Console.WriteLine("T3:{0}: entering read lock...", DateTime.Now);
    myLock.EnterReadLock();
    Console.WriteLine("T3:{0}: ...entered read lock!!!!!!!!!!!!!!!!!!!", DateTime.Now);

    System.Threading.Thread.Sleep(8000);
});

The output of this code is:

T1:18-09-2015 16:29:49: entering read lock...
T1:18-09-2015 16:29:49: ...entered read lock.
T2:18-09-2015 16:29:50: try-entering write lock...
T3:18-09-2015 16:29:51: entering read lock...
T2:18-09-2015 16:29:51: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:51: try-entering write lock...
T2:18-09-2015 16:29:53: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:53: try-entering write lock...
T2:18-09-2015 16:29:54: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:54: try-entering write lock...
T2:18-09-2015 16:29:56: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:56: try-entering write lock...
T2:18-09-2015 16:29:57: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:57: try-entering write lock...
T2:18-09-2015 16:29:59: ...try-entered write lock, result=False.
T2:18-09-2015 16:29:59: try-entering write lock...

As you can see, even though thread 2 (the "Writer") hasn't acquired a writer lock and it's not in an EnterWriteLock call, thread 3 gets blocked for good. I can see a similar behavior with ReaderWriterLock.

Am I doing anything wrong? If not, what options do I have to favor readers when a writer is queued?

like image 454
Gabriele Giuseppini Avatar asked Sep 18 '15 14:09

Gabriele Giuseppini


People also ask

What is the difference between reader/writer lock and normal lock?

The read lock may be held simultaneously by multiple reader threads, so long as there are no writers. The write lock is exclusive. So you can have many readers at a time, but only one writer - and the writer will prevent readers from reading, too.

How does read/write lock work?

ReadWriteLock is an advanced thread lock mechanism. It allows multiple threads to read a certain resource, but only one to write it, at a time. The idea is, that multiple threads can read from a shared resource without causing concurrency errors.

Why do we need read write lock?

In many situations, data is read more often than it is modified or written. In these cases, you can allow threads to read concurrently while holding the lock and allow only one thread to hold the lock when data is modified. A multiple-reader single-writer lock (or read/write lock) does this.


1 Answers

I can’t help but I believe this is a .NET Framework bug (UPDATE: I have reported the bug). The following straightforward program (a simplified version of the above) illustrates that:

var myLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);

var t1 = new Thread(() =>
{
    Console.WriteLine("T1:{0}: entering read lock...", DateTime.Now);
    myLock.EnterReadLock();
    Console.WriteLine("T1:{0}: ...entered read lock.", DateTime.Now);

    Thread.Sleep(50000);

    Console.WriteLine("T1:{0}: exiting", DateTime.Now);
    myLock.ExitReadLock();
});

var t2 = new Thread(() =>
{
    Thread.Sleep(1000);

    Console.WriteLine("T2:{0}: try-entering write lock...", DateTime.Now);
    bool result = myLock.TryEnterWriteLock(3000);
    Console.WriteLine("T2:{0}: ...try-entered write lock, result={1}.", DateTime.Now, result);

    Thread.Sleep(50000);

    if (result)
    {
        myLock.ExitWriteLock();
    }
    Console.WriteLine("T2:{0}: exiting", DateTime.Now);
});

var t3 = new Thread(() =>
{
    Thread.Sleep(2000);

    Console.WriteLine("T3:{0}: entering read lock...", DateTime.Now);
    myLock.EnterReadLock();
    Console.WriteLine("T3:{0}: ...entered read lock!!!!!!!!!!!!!!!!!!!", DateTime.Now);

    Thread.Sleep(50000);

    myLock.ExitReadLock();
    Console.WriteLine("T3:{0}: exiting", DateTime.Now);
});

t1.Start();
t2.Start();
t3.Start();

t1.Join();
t2.Join();
t3.Join();

The following happens in a simple order, no lock convoys, no races, no loops or anything.

  1. T1 acquires a read lock.
  2. T2 tries to acquire a write lock and blocks, waiting for a timeout (as T1 holds the lock).
  3. T3 tries to acquire a read lock and blocks (because T2 is blocked waiting for the write lock, and per the documentation, this means all further readers are blocked until timeouts).
  4. T2’s timeout expires. Per the documentation, T3 should now wake up and acquire the read lock. However, this does not happen and T3 is blocked forever (or, in the case of this example, for those 50 seconds until T1 leaves the read lock).

AFAICT, the ExitMyLock in ReaderWriterLockSlim’s WaitOnEvent should have been ExitAndWakeUpAppropriateWaiters.

like image 70
Mormegil Avatar answered Nov 06 '22 03:11

Mormegil