diff --git a/src/DotNext.Tests/Threading/AsyncReaderWriterLockTests.cs b/src/DotNext.Tests/Threading/AsyncReaderWriterLockTests.cs index ad3963353..a6e904f89 100644 --- a/src/DotNext.Tests/Threading/AsyncReaderWriterLockTests.cs +++ b/src/DotNext.Tests/Threading/AsyncReaderWriterLockTests.cs @@ -24,6 +24,7 @@ public static async Task TrivialLock() True(await rwLock.TryEnterReadLockAsync(DefaultTimeout)); True(await rwLock.TryUpgradeToWriteLockAsync(DefaultTimeout)); False(rwLock.TryEnterWriteLock()); + False(rwLock.TryUpgradeToWriteLock()); rwLock.DowngradeFromWriteLock(); True(await rwLock.TryEnterReadLockAsync(DefaultTimeout)); } @@ -77,7 +78,7 @@ public static void OptimisticRead() True(rwLock.Validate(stamp)); rwLock.Release(); Equal(stamp, rwLock.TryOptimisticRead()); - True(rwLock.TryEnterWriteLock()); + True(rwLock.TryEnterWriteLock(stamp)); False(rwLock.IsReadLockHeld); True(rwLock.IsWriteLockHeld); False(rwLock.Validate(stamp)); @@ -195,4 +196,69 @@ public static async Task LockStealing2() @lock.Release(); await task3; } + + [Fact] + public static void DisposedWhenSynchronousReadLockAcquired() + { + var l = new AsyncReaderWriterLock(); + True(l.TryEnterReadLock()); + + var t = new Thread(() => Throws(() => l.TryEnterWriteLock(DefaultTimeout))) { IsBackground = true }; + t.Start(); + + l.Dispose(); + True(t.Join(DefaultTimeout)); + } + + [Fact] + public static void DisposedWhenSynchronousWriteLockAcquired() + { + var l = new AsyncReaderWriterLock(); + True(l.TryEnterWriteLock()); + + var t = new Thread(() => Throws(() => l.TryEnterReadLock(DefaultTimeout))) { IsBackground = true }; + t.Start(); + + l.Dispose(); + True(t.Join(DefaultTimeout)); + } + + [Fact] + public static void AcquireReadWriteLockSynchronously() + { + var l = new AsyncReaderWriterLock(); + True(l.TryEnterReadLock(DefaultTimeout)); + True(l.TryEnterReadLock(DefaultTimeout)); + Equal(2L, l.CurrentReadCount); + + var t = new Thread(() => l.TryEnterWriteLock(DefaultTimeout)) { IsBackground = true }; + t.Start(); + + l.Release(); + l.Release(); + + True(t.Join(DefaultTimeout)); + True(l.IsWriteLockHeld); + + l.Release(); + False(l.IsWriteLockHeld); + } + + [Fact] + public static void ResumeMultipleReadersSynchronously() + { + var l = new AsyncReaderWriterLock(); + True(l.TryEnterWriteLock()); + + var t1 = new Thread(() => l.TryEnterReadLock(DefaultTimeout)) { IsBackground = true }; + var t2 = new Thread(() => l.TryEnterReadLock(DefaultTimeout)) { IsBackground = true }; + t1.Start(); + t2.Start(); + + l.Release(); + True(t1.Join(DefaultTimeout)); + True(t2.Join(DefaultTimeout)); + + Equal(2L, l.CurrentReadCount); + } } \ No newline at end of file diff --git a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs index bdb7dc4da..a28a93d72 100644 --- a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs +++ b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs @@ -93,41 +93,18 @@ public bool TryAcquire() return result; } - [UnsupportedOSPlatform("browser")] - private bool TryAcquire(Timeout timeout) - { - lock (SyncRoot) - { - while (!TryAcquireOrThrow()) - { - if (timeout.TryGetRemainingTime(out var remainingTime) && Monitor.Wait(SyncRoot, remainingTime)) - continue; - - return false; - } - } - - return true; - } - - private bool TryAcquireOrThrow() - { - ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); - return TryAcquire(ref manager); - } - /// /// Tries to acquire the lock synchronously. /// /// The interval to wait for the lock. - /// if the lock is acquired; + /// if the lock is acquired in timely manner; otherwise, . /// is negative. /// This object has been disposed. [UnsupportedOSPlatform("browser")] public bool TryAcquire(TimeSpan timeout) { ObjectDisposedException.ThrowIf(IsDisposed, this); - return timeout == TimeSpan.Zero ? TryAcquire() : TryAcquire(new Timeout(timeout)); + return timeout == TimeSpan.Zero ? TryAcquire() : TryAcquire(new Timeout(timeout), ref manager); } /// diff --git a/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs b/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs index b2fc36986..a0154a1b1 100644 --- a/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs +++ b/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Versioning; namespace DotNext.Threading; @@ -51,9 +52,10 @@ internal void DowngradeFromWriteLock() readLocks = 1L; } - internal void ExitLock() + internal bool ExitLock() { - if (writeLock) + bool result; + if (result = writeLock) { writeLock = false; readLocks = 0L; @@ -62,6 +64,8 @@ internal void ExitLock() { readLocks--; } + + return result; } internal readonly long ReadLocks => Volatile.Read(in readLocks); @@ -276,11 +280,29 @@ public LockStamp TryOptimisticRead() public bool Validate(in LockStamp stamp) => stamp.IsValid(in state); /// - /// Attempts to obtain reader lock synchronously without blocking caller thread. + /// Tries to obtain reader lock synchronously without blocking caller thread. /// - /// if lock is taken successfuly; otherwise, . + /// if lock is taken successfully; otherwise, . /// This object has been disposed. public bool TryEnterReadLock() => TryEnter(); + + /// + /// Tries to obtain reader lock synchronously. + /// + /// The time to wait. + /// if reader lock is acquired in timely manner; otherwise, . + /// is negative. + /// This object has been disposed. + [UnsupportedOSPlatform("browser")] + public bool TryEnterReadLock(TimeSpan timeout) + { + ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); + return TryEnter(timeout); + } + + private bool TryEnter(TimeSpan timeout) + where TLockManager : struct, ILockManager + => timeout == TimeSpan.Zero ? TryEnter() : TryAcquire(new Timeout(timeout), ref GetLockManager()); /// /// Tries to enter the lock in read mode asynchronously, with an optional time-out. @@ -341,9 +363,23 @@ public bool TryEnterWriteLock(in LockStamp stamp) /// /// Attempts to obtain writer lock synchronously without blocking caller thread. /// - /// if lock is taken successfuly; otherwise, . + /// if lock is taken successfully; otherwise, . /// This object has been disposed. public bool TryEnterWriteLock() => TryEnter(); + + /// + /// Tries to obtain writer lock synchronously. + /// + /// The time to wait. + /// if writer lock is acquired in timely manner; otherwise, . + /// is negative. + /// This object has been disposed. + [UnsupportedOSPlatform("browser")] + public bool TryEnterWriteLock(TimeSpan timeout) + { + ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); + return TryEnter(timeout); + } /// /// Tries to enter the lock in write mode asynchronously, with an optional time-out. @@ -387,7 +423,7 @@ public ValueTask EnterWriteLockAsync(TimeSpan timeout, CancellationToken token = /// /// Tries to upgrade the read lock to the write lock synchronously without blocking of the caller. /// - /// if lock is taken successfuly; otherwise, . + /// if lock is taken successfully; otherwise, . /// This object has been disposed. public bool TryUpgradeToWriteLock() => TryEnter(); @@ -560,11 +596,24 @@ public void Release() if (state.IsWriteLockAllowed) throw new SynchronizationLockException(ExceptionMessages.NotInLock); - state.ExitLock(); + var writeLockReleased = state.ExitLock(); suspendedCallers = DrainWaitQueue(); if (IsDisposing && IsReadyToDispose) + { Dispose(true); + Monitor.PulseAll(SyncRoot); + } + else if (writeLockReleased) + { + // assuming that we have multiple readers suspended + Monitor.PulseAll(SyncRoot); + } + else + { + // assuming that we have only one writer suspended + Monitor.Pulse(SyncRoot); + } } suspendedCallers?.Unwind(); @@ -591,6 +640,9 @@ public void DowngradeFromWriteLock() state.DowngradeFromWriteLock(); suspendedCallers = DrainWaitQueue(); + + // resume multiple readers if available + Monitor.PulseAll(SyncRoot); } suspendedCallers?.Unwind(); diff --git a/src/DotNext.Threading/Threading/QueuedSynchronizer.cs b/src/DotNext.Threading/Threading/QueuedSynchronizer.cs index 88d6e3053..8d2c43e82 100644 --- a/src/DotNext.Threading/Threading/QueuedSynchronizer.cs +++ b/src/DotNext.Threading/Threading/QueuedSynchronizer.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Metrics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Versioning; namespace DotNext.Threading; @@ -169,6 +170,32 @@ private protected bool TryAcquire(ref TLockManager manager) return true; } + [UnsupportedOSPlatform("browser")] + private protected bool TryAcquire(Timeout timeout, ref TLockManager manager) + where TLockManager : struct, ILockManager + { + lock (SyncRoot) + { + while (!TryAcquireOrThrow(ref manager)) + { + if (timeout.TryGetRemainingTime(out var remainingTime) && Monitor.Wait(SyncRoot, remainingTime)) + continue; + + return false; + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryAcquireOrThrow(ref TLockManager manager) + where TLockManager : struct, ILockManager + { + ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); + return TryAcquire(ref manager); + } + private T AcquireAsync(ref ValueTaskPool> pool, ref TLockManager manager, TInitializer initializer, TOptions options) where T : struct, IEquatable where TNode : WaitNode, IValueTaskFactory, IPooledManualResetCompletionSource>, new()