From 14da392caef225c432c9ad9abd480c9d65a21b66 Mon Sep 17 00:00:00 2001 From: sakno Date: Tue, 15 Oct 2024 15:40:02 +0300 Subject: [PATCH] Added synchronous lock acquisition support --- .../Threading/AsyncExclusiveLockTests.cs | 22 ++++-- .../Threading/AsyncExclusiveLock.cs | 67 +++++++------------ .../Threading/QueuedSynchronizer.cs | 1 + 3 files changed, 40 insertions(+), 50 deletions(-) diff --git a/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs b/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs index a70f9ffbb..7c3eb7b70 100644 --- a/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs +++ b/src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs @@ -184,12 +184,7 @@ public static async Task LockStealing2() 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)); - } + True(l.TryAcquire(DefaultTimeout)); False(l.TryAcquire(TimeSpan.Zero)); } @@ -200,7 +195,7 @@ public static async Task MixedLock() await using var l = new AsyncExclusiveLock(); True(await l.TryAcquireAsync(DefaultTimeout)); - var t = new Thread(() => l.TryAcquire(DefaultTimeout)); + var t = new Thread(() => l.TryAcquire(DefaultTimeout)) { IsBackground = true }; t.Start(); l.Release(); @@ -208,4 +203,17 @@ public static async Task MixedLock() False(l.TryAcquire()); l.Release(); } + + [Fact] + public static void DisposedWhenSynchronousLockAcquired() + { + var l = new AsyncExclusiveLock(); + True(l.TryAcquire()); + + var t = new Thread(() => Throws(() => l.TryAcquire(DefaultTimeout))) { IsBackground = true }; + t.Start(); + + l.Dispose(); + True(t.Join(DefaultTimeout)); + } } \ No newline at end of file diff --git a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs index 85879b173..bdb7dc4da 100644 --- a/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs +++ b/src/DotNext.Threading/Threading/AsyncExclusiveLock.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using System.Runtime.Versioning; namespace DotNext.Threading; @@ -16,7 +17,6 @@ public class AsyncExclusiveLock : QueuedSynchronizer, IAsyncDisposable private struct LockManager : ILockManager { private bool state; - internal ManualResetEventSlim? SyncState; internal readonly bool Value => state; @@ -27,13 +27,11 @@ private struct LockManager : ILockManager public void AcquireLock() { state = true; - SyncState?.Reset(); } internal void ExitLock() { state = false; - SyncState?.Set(); } } @@ -88,11 +86,6 @@ 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); @@ -100,50 +93,41 @@ private bool TryAcquireCore() return result; } - private bool TryAcquireCore(Timeout timeout, CancellationToken token = default) + [UnsupportedOSPlatform("browser")] + private bool TryAcquire(Timeout timeout) { - if (manager.SyncState is not { } mres) + lock (SyncRoot) { - lock (SyncRoot) + while (!TryAcquireOrThrow()) { - // Perf: avoid allocation of MRES if the lock can be acquired synchronously - if (TryAcquire(ref manager)) - return true; + if (timeout.TryGetRemainingTime(out var remainingTime) && Monitor.Wait(SyncRoot, remainingTime)) + continue; - mres = manager.SyncState ??= new(); + return false; } - - // 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; } + private bool TryAcquireOrThrow() + { + ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this); + return TryAcquire(ref manager); + } + /// /// 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) + [UnsupportedOSPlatform("browser")] + public bool TryAcquire(TimeSpan timeout) { ObjectDisposedException.ThrowIf(IsDisposed, this); - return timeout == TimeSpan.Zero ? TryAcquire() : TryAcquireCore(new(timeout), token); + return timeout == TimeSpan.Zero ? TryAcquire() : TryAcquire(new Timeout(timeout)); } /// @@ -283,21 +267,18 @@ public void Release() suspendedCaller = DrainWaitQueue(); if (IsDisposing && IsReadyToDispose) + { Dispose(true); + Monitor.PulseAll(SyncRoot); + } + else + { + Monitor.Pulse(SyncRoot); + } } suspendedCaller?.Resume(); } 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 diff --git a/src/DotNext.Threading/Threading/QueuedSynchronizer.cs b/src/DotNext.Threading/Threading/QueuedSynchronizer.cs index f5c44e185..88d6e3053 100644 --- a/src/DotNext.Threading/Threading/QueuedSynchronizer.cs +++ b/src/DotNext.Threading/Threading/QueuedSynchronizer.cs @@ -325,6 +325,7 @@ private void NotifyObjectDisposed(Exception? reason = null) lock (SyncRoot) { suspendedCallers = DetachWaitQueue()?.SetException(reason, out _); + Monitor.PulseAll(SyncRoot); } suspendedCallers?.Unwind();