4

I wrote a fairly trivial wrapper around ReaderWriterLockSlim:

class SimpleReaderWriterLock
{
    private class Guard : IDisposable
    {
        public Guard(Action action)
        {
            _Action = action;
        }

        public void Dispose()
        {
            _Action?.Invoke();
            _Action = null;
        }

        private Action _Action;
    }

    private readonly ReaderWriterLockSlim _Lock
        = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);

    public IDisposable ReadLocked()
    {
        _Lock.EnterReadLock();
        return new Guard(_Lock.ExitReadLock);
    }

    public IDisposable WriteLocked()
    {
        _Lock.EnterWriteLock();
        return new Guard(_Lock.ExitWriteLock);
    }

    public IDisposable UpgradableReadLocked()
    {
        _Lock.EnterUpgradeableReadLock();
        return new Guard(_Lock.ExitUpgradeableReadLock);
    }
}

(This is probably not the most efficient thing in the world, so I am interested in suggested improvements to this class as well.)

It is used like so:

using (_Lock.ReadLocked())
{
    // protected code
}

(There are a significant number of reads happening very frequently, and almost never any writes.)

This always seems to work as expected in Release mode and in production. However in Debug mode and in the debugger, very occasionally the process deadlocks in a peculiar state -- it has called EnterReadLock, the lock itself is not held by anything (the owner is 0, the properties that report whether it has any readers/writers/waiters say not, etc) but the spin lock inside is locked, and it's endlessly spinning there.

I don't know what triggers this, except that it seems to happen more often if I'm stopping at breakpoints and single-stepping (in completely unrelated code).

If I manually toggle the spinlock _isLocked field back to 0, then the process resumes and everything seems to work as expected afterwards.

Is there something wrong with the code or with the lock itself? Is the debugger doing something to accidentally provoke deadlocking the spinlock? (I'm using .NET 4.6.2.)

I've read an article that indicates that ThreadAbortException can be a problem for these locks -- and my code does have calls to Abort() in some places -- but I don't think those involve code which calls into this locked code (though I could be mistaken) and if the problem were that the lock had been acquired and never released then it should appear differently than what I'm seeing. (Though as an aside, the framework docs specifically ban acquiring a lock in a constrained region, as encouraged in that article.)

I can change the code to avoid the lock indirection, but aren't using guards the recommended practice in general?

Miral
  • 12,637
  • 4
  • 53
  • 93
  • What would happen if I called `WriteLocked` and then forget to call `Dispose` on the result of it? – mjwills Apr 02 '19 at 05:27
  • @mjwills What you'd expect: it would deadlock on the next read/write. **BUT** that's not what's happening here, because then the outer lock would appear locked, and that's not the case. – Miral Apr 02 '19 at 05:28

1 Answers1

1

Since the using statement is not abort-safe, you could try replacing it with the abort-safe workaround suggested in the linked article. Something like this:

public void WithReadLock(Action action)
{
    var lockAcquired = false;
    try
    {
        try { }
        finally
        {
            _Lock.EnterReadLock();
            lockAcquired = true;
        }
        action();
    }
    finally
    {
        if (lockAcquired) _Lock.ExitReadLock();
    }
}

Usage:

var locker = new SimpleReaderWriterLock();
locker.WithReadLock(() =>
{
    // protected code
});
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104