1

I'm trying to write a method which process some messages. They can be read or write messages. Parallel reads are permitted, but when write lock is aquired all subsequent read lock should wait until write lock is released. So I thught ReaderWriterLockSlim is what I need. But when I'm trying to implement simple application to see if it works as expected I get Recursive read lock acquisitions not allowed in this mode exception.

Here is my example to show how it's intended to work:

ReaderWriterLockSlim distributionLock = new ReaderWriterLockSlim();

async Task ExecuteReadLockTaskAsync(Func<Task> taskFunc)
{
    distributionLock.EnterReadLock();
    try
    {
        await taskFunc();
    }
    finally
    {
        if (distributionLock.IsReadLockHeld)
        {
            distributionLock.ExitReadLock();
        }
    }
}

async Task ExecuteWriteLockTaskAsync(Func<Task> taskFunc)
{
    distributionLock.EnterWriteLock();
    try
    {
        await taskFunc();
    }
    finally
    {
        if (distributionLock.IsWriteLockHeld)
        {
            distributionLock.ExitWriteLock();
        }
    }
}

Task ProcessAsync(bool flag)
{
    switch (flag)
    {
        case false:
            return ExecuteReadLockTaskAsync(() =>
            {
                Console.WriteLine("Readonly task start");
                return Task.Delay(1000).ContinueWith(t => Console.WriteLine("Readonly task done"));
            });
        case true:
            return ExecuteWriteLockTaskAsync(() =>
            {
                Console.WriteLine("Write task start");
                return Task.Delay(3000).ContinueWith(t => Console.WriteLine("Write task done"));
            });
        default:
            throw new InvalidOperationException($"Unknown message typex");
    }
}

var tasks=  new List<Task>();
for (int i = 0; i < 100; i++)
{
    tasks.Add(ProcessAsync(false));
}
tasks.Add(ProcessAsync(true));

for (int i = 0; i < 100; i++)
{
    tasks.Add(ProcessAsync(false));
}

await Task.WhenAll(tasks);

Expected result: 100 lines of Readonly task start, one line of Write task start, then 100 lines of Readonly task done, then one line of Write task done, then the rest of program prints.

Actual result:

Readonly task start
Readonly task done

LockRecursionException4
Recursive read lock acquisitions not allowed in this mode. 

I don't get where recursion appears here. I'm just calling one function, without any kind of recursion. I read articles but I don't see how it works here.

Alex Zhukovskiy
  • 9,565
  • 11
  • 75
  • 151
  • Possible duplicate of [Is it Safe to use ReaderWriterLockSlim in an async method](https://stackoverflow.com/questions/15882710/is-it-safe-to-use-readerwriterlockslim-in-an-async-method). (The answer is "no".) – Jeroen Mostert Jun 20 '19 at 11:45
  • My error is different. Howerver, this link is useful, thanks. – Alex Zhukovskiy Jun 20 '19 at 11:46
  • Different error, but same root cause -- the lock won't be released if the `await` ends up on a different thread from when it started (as it doesn't think it's holding the lock), and when the task eventually does run on a thread that is still holding the lock, you'll get this error. – Jeroen Mostert Jun 20 '19 at 11:50
  • Yes, I get it. Then it's actually a duplicate. But I don't see any answer how can I emulate then to make it work. Maybe I should change question subject? – Alex Zhukovskiy Jun 20 '19 at 11:51
  • 1
    Stephen Cleary's answer links to his [AsyncEx](https://github.com/StephenCleary/AsyncEx) library, which has an `AsyncReaderWriterLock`. – Jeroen Mostert Jun 20 '19 at 11:52
  • Yeah, I used it and it works as expected. Thanks. – Alex Zhukovskiy Jun 20 '19 at 11:59
  • I recommend **not** using RWLs of any kind unless performance testing shows it would be useful. RWLs are only appropriate when there is a split between readers and writers **and** writers are rare **and** there's a lot of contention between readers. Otherwise, just use a plain lock (`SemaphoreSlim` in the async case). In particular, if an app does not have a lot of lock contention, then it doesn't need a RWL. – Stephen Cleary Jun 20 '19 at 12:36
  • @StephenCleary I have a lot of requests that should be processed in parallel unless there are some other requests, then these requests should wait until the second ones proceed. Why should I rewrite these primitives myself instead of just taking existing ones? Is there really big problems with entering in read locks? – Alex Zhukovskiy Jun 20 '19 at 17:43
  • @AlexZhukovskiy: Jumping to RWL is a common problem, and it often is not a good match to the problem. E.g., what you're trying to do is *prioritize work,* not *manage exclusive access.* What happens next month when you need to add another priority level? And a RWL wouldn't allow the second requests to operate in parallel. – Stephen Cleary Jun 20 '19 at 18:03
  • No, I want exlusive access, not some kind of priority. It's actually some sort of caching data and updating it. I won't have a "third" option here. I only can get data or update it, nothing more. – Alex Zhukovskiy Jun 21 '19 at 22:23

0 Answers0