0

I have a function to clean up some objects as well as the ReaderWriterLockSlim. But I need the ReaderWriterLockSlim to lock as writer lock to prevent the other thread read the data while I am doing the clean up.

ConcurrentDictionary<string, ReaderWriterLockSlim> RwLocks = new ConcurrentDictionary<string, ReaderWriterLockSlim>();

private ReaderWriterLockSlim GetRwLock(string key)
{
    return RwLocks.GetOrAdd(key, _ => new ReaderWriterLockSlim());
}

public void CleanUp(string key)
{
    ReaderWriterLockSlim rwLock = this.GetRwLock(key);

    try 
    {
        rwLock.EnterWriterLock();
        // do some other clean up
        this.RwLocks.TryRemove(key, out _);
    }
    finally
    {
        rwLock.ExitWriterLock();
        // It is safe to do dispose here?
        // could other thread enter the read lock or writer lock here?
        // and the dispose will throw exceptions?
        // What is the best practice to do the dispose?
        rwLock.Dispose();
    }
}

I have an idea to wrap the ReaderWriterLockSlim. Do you think it could solve the problem or have any potential risk

public class ReaderWriterLockSlimWrapper
{
    private ReaderWriterLockSlim rwLock;

    private volatile bool disposed;

    public ReaderWriterLockSlimWrapper()
    {
        rwLock = new ReaderWriterLockSlim();
        disposed = false;
    }

    private void DisposeInternal()
    {
        if (!rwLock.IsReadLockHeld && !rwLock.IsUpgradeableReadLockHeld && !rwLock.IsWriteLockHeld)
        {
            rwLock.Dispose();
        }
    }

    public void Dispose()
    {
        disposed = true;

        DisposeInternal();
    }

    public void EnterReadLock()
    {
        rwLock.EnterReadLock();
    }

    public void ExitReadLock()
    {
        rwLock.ExitReadLock();

        if (disposed)
        {
            DisposeInternal();
        }
    }

    public void EnterWriteLock()
    {
        rwLock.EnterWriteLock();
    }

    public void ExitWriteLock()
    {
        rwLock.ExitWriteLock();

        if (disposed)
        {
            DisposeInternal();
        }
    }
}
leuction
  • 563
  • 2
  • 8
  • 19
  • Since you have removed the RWL from the collection and the GetRwLock creates a new one if needed that's why it seems fine to call Dispose there. – Peter Csala May 13 '22 at 05:41
  • Related: [Asynchronous locking based on a key](https://stackoverflow.com/questions/31138179/asynchronous-locking-based-on-a-key) / [ConcurrentDictionary with multiple values per key, removing empty entries](https://stackoverflow.com/questions/60695167/concurrentdictionary-with-multiple-values-per-key-removing-empty-entries). One way or another, it's a tricky business. It's much simpler to just leave the `ReaderWriterLockSlim`s hanging around until the completion of the program. – Theodor Zoulias May 13 '22 at 07:12
  • @PeterCsala But other threads could be blocked with EnterReadLock() when we exit the writer lock. will other threads enter the reader lock or try to get the reader lock again? – leuction May 13 '22 at 07:35
  • If that could happen then `EnterReadLock` calls will result in `ObjectDisposedException`. – Peter Csala May 13 '22 at 07:38
  • The same is true if you would use `TryEnterXYZ` methods. – Peter Csala May 13 '22 at 07:40
  • @TheodorZoulias if we hanging the `ReaderWriterLockSlim` around, could we cause any resource leak? – leuction May 13 '22 at 07:40
  • Sure, keeping in memory objects that are no longer used is a memory leak. Whether this is significant depends on how many gonna be these unused objects, and how many bytes each one of them reserves. – Theodor Zoulias May 13 '22 at 08:05
  • 1
    I'd take a step back from here - how is it that, on the one hand you have some code that knows that it is now "the right time" to call `CleanUp` and, on the other hand, you don't know if other threads are going to try to enter the lock? That shouldn't be a situation you get yourself into in the first place. – Damien_The_Unbeliever May 13 '22 at 09:39
  • @Damien_The_Unbeliever I want to do a conditional clean-up to save the memory. I have some thread which holds the reader lock to read/write the data. I also have another thread periodically locks the writer lock to block the other do read/write during the clean-up. you can think it as GC – leuction May 13 '22 at 16:52
  • @Damien_The_Unbeliever But the problem is when I want to delete the entry, I also want to delete the readerwriterlock just in case there is too many reader writer lock in the memory, that will cause the memory leak. For this kind of situation, do you have any better idea? – leuction May 13 '22 at 17:03
  • 1
    Well, at this point you seem to be describing a cache of some kind. It's unclear *why you're trying to build this yourself* versus using already built caching systems, so, *again*, take a step back. – Damien_The_Unbeliever May 13 '22 at 17:51

1 Answers1

0

You haven't described the specific scenario where you intend to use your two mechanisms, neither for the first one with the CleanUp/GetRwLock methods, nor for the second one with the ReaderWriterLockSlimWrapper class. So I guess the question is:

Are my two mechanisms safe to use in all possible multithreaded scenarios, where thread-safety and atomicity of operations is mandatory?

The answer is no, both of your mechanisms are riddled with race conditions, and offer no guarantees about atomicity. Using them in a multithreaded scenario would result in undefined behavior, including but not limited to:

  1. Unexpected exceptions.
  2. Violations of the policies that a correctly used ReaderWriterLockSlim is normally expected to enforce. In order words it is possible that two threads will acquire a writer lock for the same key concurrently to each other, or concurrently with others threads that have acquired a reader lock for the same key, or both.

Explaining why your mechanisms are flawed is quite involved. A general explanation is that whenever you use the pattern if (x.BooleanProperty) x.Method(); in a multithreaded environment, although the BooleanProperty and the Method might be individually thread-safe, you are allowing a second thread to preempt the current thread between the two invocations, and invalidate the result of the first check.

As a side note, be aware that the ReaderWriterLockSlim is not a cross-process synchronization primitive. So even if you fix your mechanisms and then attempt to use them in a web application, the policies might still be violated because the web server might decide at random moments to recycle the current process and start a new one. In that case the web application might by running concurrently on two processes, for a period of time that spans a few seconds or even minutes.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104