How would you simplify Entering and Exiting a ReaderWriterLock?
Asked Answered
M

5

11

This seems very noisy to me. Five lines of overhead is just too much.

m_Lock.EnterReadLock()
Try
    Return m_List.Count
Finally
    m_Lock.ExitReadLock()
End Try

So how would you simply this?

Maverick answered 4/10, 2008 at 9:16 Comment(0)
B
23

I was thinking the same, but in C# ;-p

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        ReaderWriterLockSlim sync = new ReaderWriterLockSlim();

        using (sync.Read())
        {
           // etc    
        }
    }


}
public static class ReaderWriterExt
{
    sealed class ReadLockToken : IDisposable
    {
        private ReaderWriterLockSlim sync;
        public ReadLockToken(ReaderWriterLockSlim sync)
        {
            this.sync = sync;
            sync.EnterReadLock();
        }
        public void Dispose()
        {
            if (sync != null)
            {
                sync.ExitReadLock();
                sync = null;
            }
        }
    }
    public static IDisposable Read(this ReaderWriterLockSlim obj)
    {
        return new ReadLockToken(obj);
    }
}
Bab answered 4/10, 2008 at 9:24 Comment(5)
Funny, I had long since forgotten about this and just by chance it pops in my reputation points up just as I could benefit from it.Maverick
what about write ?Bless
@Bless same thing with EnterWriteLock?Bab
Any reason not to use structs here for the token @MarcGravell?Inharmonious
@YairHalberstadt the mutability bit is a change; personally, I would use a struct here, probably a readonly struct, and if somebody calls dispose twice: that's their problem for using it incorrectly, but... it is a changeBab
S
7

All the solutions posted so far are at risk of deadlock. A using block like this:

ReaderWriterLockSlim sync = new ReaderWriterLockSlim();
using (sync.Read())
{
  // Do stuff
}

gets converted into something like this:

ReaderWriterLockSlim sync = new ReaderWriterLockSlim();
IDisposable d = sync.Read();
try
{
  // Do stuff
}
finally
{
  d.Dispose();
}

This means that a ThreadAbortException (or similar) could happen between sync.Read() and the try block. When this happens the finally block never gets called, and the lock is never released!

For more information, and a better implementation see: Deadlock with ReaderWriterLockSlim and other lock objects. In short, the better implementation comes down to moving the lock into the try block like so:

ReaderWriterLockSlim myLock = new ReaderWriterLockSlim();
try
{
    myLock.EnterReadLock();
    // Do stuff
}
finally
{
    // Release the lock
    myLock.ExitReadLock();
}

A wrapper class like the one in the accepted answer would be:

  /// <summary>
  /// Manager for a lock object that acquires and releases the lock in a manner
  /// that avoids the common problem of deadlock within the using block
  /// initialisation.
  /// </summary>
  /// <remarks>
  /// This manager object is, by design, not itself thread-safe.
  /// </remarks>
  public sealed class ReaderWriterLockMgr : IDisposable
  {
    /// <summary>
    /// Local reference to the lock object managed
    /// </summary>
    private ReaderWriterLockSlim _readerWriterLock = null;

    private enum LockTypes { None, Read, Write, Upgradeable }
    /// <summary>
    /// The type of lock acquired by this manager
    /// </summary>
    private LockTypes _enteredLockType = LockTypes.None;

    /// <summary>
    /// Manager object construction that does not acquire any lock
    /// </summary>
    /// <param name="ReaderWriterLock">The lock object to manage</param>
    public ReaderWriterLockMgr(ReaderWriterLockSlim ReaderWriterLock)
    {
      if (ReaderWriterLock == null)
        throw new ArgumentNullException("ReaderWriterLock");
      _readerWriterLock = ReaderWriterLock;
    }

    /// <summary>
    /// Call EnterReadLock on the managed lock
    /// </summary>
    public void EnterReadLock()
    {
      if (_readerWriterLock == null)
        throw new ObjectDisposedException(GetType().FullName);
      if (_enteredLockType != LockTypes.None)
        throw new InvalidOperationException("Create a new ReaderWriterLockMgr for each state you wish to enter");
      // Allow exceptions by the Enter* call to propogate
      // and prevent updating of _enteredLockType
      _readerWriterLock.EnterReadLock();
      _enteredLockType = LockTypes.Read;
    }

    /// <summary>
    /// Call EnterWriteLock on the managed lock
    /// </summary>
    public void EnterWriteLock()
    {
      if (_readerWriterLock == null)
        throw new ObjectDisposedException(GetType().FullName);
      if (_enteredLockType != LockTypes.None)
        throw new InvalidOperationException("Create a new ReaderWriterLockMgr for each state you wish to enter");
      // Allow exceptions by the Enter* call to propogate
      // and prevent updating of _enteredLockType
      _readerWriterLock.EnterWriteLock();
      _enteredLockType = LockTypes.Write;
    }

    /// <summary>
    /// Call EnterUpgradeableReadLock on the managed lock
    /// </summary>
    public void EnterUpgradeableReadLock()
    {
      if (_readerWriterLock == null)
        throw new ObjectDisposedException(GetType().FullName);
      if (_enteredLockType != LockTypes.None)
        throw new InvalidOperationException("Create a new ReaderWriterLockMgr for each state you wish to enter");
      // Allow exceptions by the Enter* call to propogate
      // and prevent updating of _enteredLockType
      _readerWriterLock.EnterUpgradeableReadLock();
      _enteredLockType = LockTypes.Upgradeable;
    }

    /// <summary>
    /// Exit the lock, allowing re-entry later on whilst this manager is in scope
    /// </summary>
    /// <returns>Whether the lock was previously held</returns>
    public bool ExitLock()
    {
      switch (_enteredLockType)
      {
        case LockTypes.Read:
          _readerWriterLock.ExitReadLock();
          _enteredLockType = LockTypes.None;
          return true;
        case LockTypes.Write:
          _readerWriterLock.ExitWriteLock();
          _enteredLockType = LockTypes.None;
          return true;
        case LockTypes.Upgradeable:
          _readerWriterLock.ExitUpgradeableReadLock();
          _enteredLockType = LockTypes.None;
          return true;
      }
      return false;
    }

    /// <summary>
    /// Dispose of the lock manager, releasing any lock held
    /// </summary>
    public void Dispose()
    {
      if (_readerWriterLock != null)
      {
        ExitLock();
        // Tidy up managed resources
        // Release reference to the lock so that it gets garbage collected
        // when there are no more references to it
        _readerWriterLock = null;
        // Call GC.SupressFinalize to take this object off the finalization
        // queue and prevent finalization code for this object from
        // executing a second time.
        GC.SuppressFinalize(this);
      }
    }

    protected ~ReaderWriterLockMgr()
    {
      if (_readerWriterLock != null)
        ExitLock();
      // Leave references to managed resources so that the garbage collector can follow them
    }
  }

Making usage as follows:

ReaderWriterLockSlim myLock = new ReaderWriterLockSlim();

using (ReaderWriterLockMgr lockMgr = new ReaderWriterLockMgr(myLock))
{
    lockMgr.EnterReadLock();
    // Do stuff
}

Also, from Joe Duffy's Blog

Next, the lock is not robust to asynchronous exceptions such as thread aborts and out of memory conditions. If one of these occurs while in the middle of one of the lock’s methods, the lock state can be corrupt, causing subsequent deadlocks, unhandled exceptions, and (sadly) due to the use of spin locks internally, a pegged 100% CPU. So if you’re going to be running your code in an environment that regularly uses thread aborts or attempts to survive hard OOMs, you’re not going to be happy with this lock.

Stealage answered 4/8, 2010 at 14:24 Comment(6)
If someone is throwing around ThreadAbortExceptions then there are far more serious problems than just a deadlock. Then ONLY time a ThreadAbortException is appropriate is when it is raised by the thread itself such as when calling HttpResponse.End.Maverick
I think this is a great point and should receive more attention. I was really attracted to Marc Gravell's answer until I read this.Drescher
... both links are dead.Paw
"... and the lock is never released!" but doesn't the garbage collector will call it somewhat later in time because d is no longer referenced? So the "problem" is not that important in most cases. Just a guess...Paw
@Paw The GC don't call Dispose, but you can have a destructor in the class that calls Dispose: https://mcmap.net/q/277377/-does-garbage-collector-call-dispose-duplicate/681538Huesman
Right, that was not pointed out clear enough in my statement. I assumed that the class has a call in the destructor and is correctly implemented as it should be. So the GC calls it implicitly then.Paw
B
2

This is not my invention but it certainly has made by hair a little less gray.

internal static class ReaderWriteLockExtensions
{
    private struct Disposable : IDisposable
    {
        private readonly Action m_action;
        private Sentinel m_sentinel;

        public Disposable(Action action)
        {
            m_action = action;
            m_sentinel = new Sentinel();
        }

        public void Dispose()
        {
            m_action();
            GC.SuppressFinalize(m_sentinel);
        }
    }

    private class Sentinel
    {
        ~Sentinel()
        {
            throw new InvalidOperationException("Lock not properly disposed.");
        }
    }

    public static IDisposable AcquireReadLock(this ReaderWriterLockSlim lock)
    {
        lock.EnterReadLock();
        return new Disposable(lock.ExitReadLock);
    }

    public static IDisposable AcquireUpgradableReadLock(this ReaderWriterLockSlim lock)
    {
        lock.EnterUpgradeableReadLock();
        return new Disposable(lock.ExitUpgradeableReadLock);
    }

    public static IDisposable AcquireWriteLock(this ReaderWriterLockSlim lock)
    {
        lock.EnterWriteLock();
        return new Disposable(lock.ExitWriteLock);
    }
} 

How to use:

using (m_lock.AcquireReadLock())
{
    // Do stuff
}
Blear answered 25/11, 2008 at 14:6 Comment(2)
learn.microsoft.com/en-us/visualstudio/code-quality/ca1065 Throwing an exception from a finalizer causes the CLR to fail fast, which tears down the process. Therefore, throwing exceptions in a finalizer should always be avoided.Sites
Well, in a case like this is shows a greater design flaw, so failing fast would be preferred.Blear
M
0

I ended up doing this, but I'm still open to better ways or flaws in my design.

Using m_Lock.ReadSection
    Return m_List.Count
End Using

This uses this extension method/class:

<Extension()> Public Function ReadSection(ByVal lock As ReaderWriterLockSlim) As ReadWrapper
    Return New ReadWrapper(lock)
End Function


Public NotInheritable Class ReadWrapper
    Implements IDisposable

    Private m_Lock As ReaderWriterLockSlim
    Public Sub New(ByVal lock As ReaderWriterLockSlim)
        m_Lock = lock
        m_Lock.EnterReadLock()
    End Sub
    Public Sub Dispose() Implements IDisposable.Dispose
        m_Lock.ExitReadLock()
    End Sub

End Class
Maverick answered 4/10, 2008 at 9:18 Comment(2)
Two thoughts: first, you should clear m_Lock so that a double Dispose() doesn't cause issues (unlikely, but...) second - there is no need for the caller to know about ReadWrapper if IDisposable would suffice. But I like it ;-pBab
Good point, I didn't want to expose the ReadWrapper type anyways.Maverick
P
0

Since the point of a lock is to protect some piece of memory, I think it would be useful to wrap that memory in a "Locked" object, and only make it accessble through the various lock tokens (as mentioned by Mark):

// Stores a private List<T>, only accessible through lock tokens
//  returned by Read, Write, and UpgradableRead.
var lockedList = new LockedList<T>( );
using( var r = lockedList.Read( ) ) {
  foreach( T item in r.Reader )
    ...
}
using( var w = lockedList.Write( ) ) {
  w.Writer.Add( new T( ) );
}
T t = ...;
using( var u = lockedList.UpgradableRead( ) ) {
  if( !u.Reader.Contains( t ) )
    using( var w = u.Upgrade( ) )
      w.Writer.Add( t );
}

Now the only way to access the internal list is by calling the appropriate accessor.

This works particularly well for List<T>, since it already has the ReadOnlyCollection<T> wrapper. For other types, you could always create a Locked<T,T>, but then you lose out on the nice readable/writable type distinction.

One improvement might be to define the R and W types as disposable wrappers themselves, which would protected against (inadvertant) errors like:

List<T> list;
using( var w = lockedList.Write( ) )
  list = w.Writable;

//BAD: "locked" object leaked outside of lock scope
list.MakeChangesWithoutHoldingLock( );

However, this would make Locked more complicated to use, and the current version does gives you the same protection you have when manually locking a shared member.


sealed class LockedList<T> : Locked<List<T>, ReadOnlyCollection<T>> {
  public LockedList( )
    : base( new List<T>( ), list => list.AsReadOnly( ) )
  { }
}

public class Locked<W, R> where W : class where R : class {
  private readonly LockerState state_;
  public Locked( W writer, R reader ) { this.state_ = new LockerState( reader, writer ); }
  public Locked( W writer, Func<W, R> getReader ) : this( writer, getReader( writer ) ) { }

  public IReadable Read( ) { return new Readable( this.state_ ); }
  public IWritable Write( ) { return new Writable( this.state_ ); }
  public IUpgradable UpgradableRead( ) { return new Upgradable( this.state_ ); }


  public interface IReadable : IDisposable { R Reader { get; } }
  public interface IWritable : IDisposable { W Writer { get; } }
  public interface IUpgradable : IReadable { IWritable Upgrade( );}


  #region Private Implementation Details
  sealed class LockerState {
    public readonly R Reader;
    public readonly W Writer;
    public readonly ReaderWriterLockSlim Sync;

    public LockerState( R reader, W writer ) {
      Debug.Assert( reader != null && writer != null );
      this.Reader = reader;
      this.Writer = writer;
      this.Sync = new ReaderWriterLockSlim( );
    }
  }

  abstract class Accessor : IDisposable {
    private LockerState state_;
    protected LockerState State { get { return this.state_; } }
    protected Accessor( LockerState state ) {
      Debug.Assert( state != null );
      this.Acquire( state.Sync );
      this.state_ = state;
    }

    protected abstract void Acquire( ReaderWriterLockSlim sync );
    protected abstract void Release( ReaderWriterLockSlim sync );

    public void Dispose( ) {
      if( this.state_ != null ) {
        var sync = this.state_.Sync;
        this.state_ = null;
        this.Release( sync );
      }
    }
  }

  class Readable : Accessor, IReadable {
    public Readable( LockerState state ) : base( state ) { }
    public R Reader { get { return this.State.Reader; } }
    protected override void Acquire( ReaderWriterLockSlim sync ) { sync.EnterReadLock( ); }
    protected override void Release( ReaderWriterLockSlim sync ) { sync.ExitReadLock( ); }
  }

  sealed class Writable : Accessor, IWritable {
    public Writable( LockerState state ) : base( state ) { }
    public W Writer { get { return this.State.Writer; } }
    protected override void Acquire( ReaderWriterLockSlim sync ) { sync.EnterWriteLock( ); }
    protected override void Release( ReaderWriterLockSlim sync ) { sync.ExitWriteLock( ); }
  }

  sealed class Upgradable : Readable, IUpgradable {
    public Upgradable( LockerState state ) : base( state ) { }
    public IWritable Upgrade( ) { return new Writable( this.State ); }
    protected override void Acquire( ReaderWriterLockSlim sync ) { sync.EnterUpgradeableReadLock( ); }
    protected override void Release( ReaderWriterLockSlim sync ) { sync.ExitUpgradeableReadLock( ); }
  }
  #endregion
}
Plummy answered 18/10, 2008 at 23:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.