diff --git a/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs b/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs index 2393065e9..a70f9ffbb 100644 --- a/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs +++ b/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs @@ -179,4 +179,33 @@ public static async Task LockStealing2() l.Release(); await task3; } + + [Fact] + public static void SynchronousLock() + { + using var l = new AsyncExclusiveLock(); + True(l.TryAcquire(DefaultTimeout, CancellationToken.None)); + + using (var cts = new CancellationTokenSource(100)) + { + Throws(() => l.TryAcquire(DefaultTimeout, cts.Token)); + } + + False(l.TryAcquire(TimeSpan.Zero)); + } + + [Fact] + public static async Task MixedLock() + { + await using var l = new AsyncExclusiveLock(); + True(await l.TryAcquireAsync(DefaultTimeout)); + + var t = new Thread(() => l.TryAcquire(DefaultTimeout)); + t.Start(); + l.Release(); + + True(t.Join(DefaultTimeout)); + False(l.TryAcquire()); + l.Release(); + } } \ No newline at end of file diff --git a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs index 76effb6af..85879b173 100644 --- a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs +++ b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs @@ -16,6 +16,7 @@ public class AsyncExclusiveLock : QueuedSynchronizer, IAsyncDisposable private struct LockManager : ILockManager { private bool state; + internal ManualResetEventSlim? SyncState; internal readonly bool Value => state; @@ -23,9 +24,17 @@ private struct LockManager : ILockManager public readonly bool IsLockAllowed => !state; - public void AcquireLock() => state = true; + public void AcquireLock() + { + state = true; + SyncState?.Reset(); + } - internal void ExitLock() => state = false; + internal void ExitLock() + { + state = false; + SyncState?.Set(); + } } private ValueTaskPool> pool; @@ -79,6 +88,11 @@ private void OnCompleted(DefaultWaitNode node) public bool TryAcquire() { ObjectDisposedException.ThrowIf(IsDisposed, this); + return TryAcquireCore(); + } + + private bool TryAcquireCore() + { Monitor.Enter(SyncRoot); var result = TryAcquire(ref manager); Monitor.Exit(SyncRoot); @@ -86,13 +100,59 @@ public bool TryAcquire() return result; } + private bool TryAcquireCore(Timeout timeout, CancellationToken token = default) + { + if (manager.SyncState is not { } mres) + { + lock (SyncRoot) + { + // Perf: avoid allocation of MRES if the lock can be acquired synchronously + if (TryAcquire(ref manager)) + return true; + + mres = manager.SyncState ??= new(); + } + + // lock status is already checked, go to the loop + } + else if (TryAcquireCore()) + { + return true; + } + + do + { + if (timeout.TryGetRemainingTime(out var remainingTime) && mres.Wait(remainingTime, token)) + continue; + + return false; + } while (!TryAcquireCore()); + + return true; + } + + /// + /// Tries to acquire the lock synchronously. + /// + /// The interval to wait for the lock. + /// The token that can be used to abort lock acquisition. + /// if the lock is acquired; + /// is negative. + /// This object has been disposed. + /// The operation has been canceled. + public bool TryAcquire(TimeSpan timeout, CancellationToken token = default) + { + ObjectDisposedException.ThrowIf(IsDisposed, this); + return timeout == TimeSpan.Zero ? TryAcquire() : TryAcquireCore(new(timeout), token); + } + /// /// Tries to enter the lock in exclusive mode asynchronously, with an optional time-out. /// /// The interval to wait for the lock. /// The token that can be used to abort lock acquisition. /// if the caller entered exclusive mode; otherwise, . - /// Time-out value is negative. + /// is negative. /// This object has been disposed. /// The operation has been canceled. /// The operation has been interrupted manually. @@ -105,7 +165,7 @@ public ValueTask TryAcquireAsync(TimeSpan timeout, CancellationToken token /// The interval to wait for the lock. /// The token that can be used to abort lock acquisition. /// The task representing lock acquisition operation. - /// Time-out value is negative. + /// is negative. /// This object has been disposed. /// The lock cannot be acquired during the specified amount of time. /// The operation has been canceled. @@ -230,4 +290,14 @@ public void Release() } private protected sealed override bool IsReadyToDispose => manager is { Value: false } && WaitQueueHead is null; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + manager.SyncState?.Dispose(); + } + + base.Dispose(disposing); + } } \ No newline at end of file