0

What is the main difference of ReaderWriterLockSlim than regular Lock?

Since I am trying to achieve async, await, I can't use regular Lock

So I am trying to make it work with ReaderWriterLockSlim

However I am getting this error,

System.Threading.LockRecursionException: 'Recursive write lock acquisitions not allowed in this mode.'

Isn't supposed ReaderWriterLockSlim to make queued threads/tasks to wait until released? So when the currently working thread on the given method is done, isn't the next in queued thread/task supposed to enter the method?

So lets say I have 5 tasks that wanted to access my method written as below and let's name them as A,B,C,D,E

Let's say B got the method and _lockRootAdd is now locked. Isn't A,C,D,E tasks supposed to wait until lock is released and once released aren't they supposed to enter the method 1 by 1?

private static readonly ReaderWriterLockSlim _lockRootAdd = new ReaderWriterLockSlim();

    private static async Task<int> returnRootDomainId(this string srUrl)
    {
        _lockRootAdd.EnterWriteLock();
        try
        {
            using ExampleCrawlerContext _context = new ExampleCrawlerContext();
            string rootDomain = srUrl.NormalizeUrl().returnRootDomainUrl();
            var rootDomainHash = rootDomain.SHA256Hash();

            var result = await _context.RootDomains.Where(pr => pr.RootDomainUrlHash == rootDomainHash).FirstOrDefaultAsync().ConfigureAwait(false);
            if (result == null)
            {
                RootDomains _RootDomain = new RootDomains();
                _RootDomain.RootDomainUrlHash = rootDomainHash;

                _context.Add(_RootDomain);
                await _context.SaveChangesAsync().ConfigureAwait(false);

                await addUrl(rootDomain).ConfigureAwait(false);
            }

            var result2 = await _context.RootDomains.Where(pr => pr.RootDomainUrlHash == rootDomainHash).FirstOrDefaultAsync().ConfigureAwait(false);
            return result2.RootDomainId;
        }
        finally
        {
            _lockRootAdd.ExitWriteLock();
        }
    }

enter image description here

Furkan Gözükara
  • 22,964
  • 77
  • 205
  • 342
  • Have you tried setting the lock’s [RecursionPolicy](https://learn.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlockslim.recursionpolicy?view=net-7.0) to [SupportsRecursion](https://learn.microsoft.com/en-us/dotnet/api/system.threading.lockrecursionpolicy?view=net-7.0)? – stuartd Jan 21 '23 at 14:23
  • The problem is that during the `await` inside the lock, the thread is allowed to do other work. One of the things it might be asked to do is `returnRootDomainId()`. If that happens, then you have a single thread acquiring the same slim lock twice, which is a lock recursion violation. In general, awaiting while holding a lock is a bad idea because it leads to problems like this. (Another common failure mode is a deadlock, where a thread is waiting to acquire a lock in read mode that it already owns in write mode.) – Raymond Chen Jan 21 '23 at 14:47
  • @stuartd that is what i want to prevent. I want only 1 thread to enter at a time – Furkan Gözükara Jan 21 '23 at 14:49
  • @RaymondChen i agree. but i am not calling returnRootDomainId inside returnRootDomainId twice. if i understand correctly, same thread is somehow calling returnRootDomainId twice. however if different threads call it, then this works. how can we prevent same thread entering inside it twice as well and make them behave as different threads called it? – Furkan Gözükara Jan 21 '23 at 14:53
  • @RaymondChen so even if same threads calls it, calls should be queued like different threads called it. this is the behaviour when we use lock – Furkan Gözükara Jan 21 '23 at 14:53
  • Also, your ConfigureAwait(false) means that the await can resume on a different thread. This means that your final ExitWriteLock may not occur on the same thread as the EnterWriteLock, which is another lock violation. What you need is an [awaitable lock](https://stackoverflow.com/questions/32654509/awaitable-autoresetevent). – Raymond Chen Jan 21 '23 at 14:53
  • @RaymondChen that came to my mind but ReaderWriterLockSlim is a static object. so all threads using the same ReaderWriterLockSlim object . therefore it shouldn't make difference? – Furkan Gözükara Jan 21 '23 at 14:56
  • 4
    `ReaderWriterLockSlim` isn't intended for use with async; see e.g. [here](https://stackoverflow.com/a/19664437/2385218). If all you're after is mutex in an async context, `SemaphoreSlim` is usually the go-to type. – sellotape Jan 21 '23 at 14:57
  • @sellotape if i change code to SemaphoreSlim should work directly or i would need other changes? gonna test now – Furkan Gözükara Jan 21 '23 at 15:02
  • Should work as-is. Create it as `new SemaphoreSlim(1, 1)` (the parameters logic is somewhat confusing). – sellotape Jan 21 '23 at 15:09
  • @sellotape thank you looks like working much better :) – Furkan Gözükara Jan 21 '23 at 15:51

1 Answers1

2

As noted in the comments, ReaderWriterLockSlim doesn't support await. The core problem is that traditional locks are thread-affine, and await can change threads.

The only out-of-the-box solution for await-compatible mutual exclusion is SemaphoreSlim:

private static readonly SemaphoreSlim _lockRootAdd = new SemaphoreSlim(1, 1);

private static async Task<int> returnRootDomainId(this string srUrl)
{
  await _lockRootAdd.WaitAsync();
  try
  {
    ...
  }
  finally
  {
    _lockRootAdd.Release();
  }
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810