Skip to content

Commit

Permalink
Added synchronous lock acquisition support
Browse files Browse the repository at this point in the history
  • Loading branch information
sakno committed Oct 15, 2024
1 parent dce752b commit 14da392
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 50 deletions.
22 changes: 15 additions & 7 deletions src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OperationCanceledException>(() => l.TryAcquire(DefaultTimeout, cts.Token));
}
True(l.TryAcquire(DefaultTimeout));

False(l.TryAcquire(TimeSpan.Zero));
}
Expand All @@ -200,12 +195,25 @@ 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();

True(t.Join(DefaultTimeout));
False(l.TryAcquire());
l.Release();
}

[Fact]
public static void DisposedWhenSynchronousLockAcquired()
{
var l = new AsyncExclusiveLock();
True(l.TryAcquire());

var t = new Thread(() => Throws<ObjectDisposedException>(() => l.TryAcquire(DefaultTimeout))) { IsBackground = true };
t.Start();

l.Dispose();
True(t.Join(DefaultTimeout));
}
}
67 changes: 24 additions & 43 deletions src/DotNext.Threading/Threading/AsyncExclusiveLock.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace DotNext.Threading;

Expand All @@ -16,7 +17,6 @@ public class AsyncExclusiveLock : QueuedSynchronizer, IAsyncDisposable
private struct LockManager : ILockManager<DefaultWaitNode>
{
private bool state;
internal ManualResetEventSlim? SyncState;

internal readonly bool Value => state;

Expand All @@ -27,13 +27,11 @@ private struct LockManager : ILockManager<DefaultWaitNode>
public void AcquireLock()
{
state = true;
SyncState?.Reset();
}

internal void ExitLock()
{
state = false;
SyncState?.Set();
}
}

Expand Down Expand Up @@ -88,62 +86,48 @@ 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);

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);
}

/// <summary>
/// Tries to acquire the lock synchronously.
/// </summary>
/// <param name="timeout">The interval to wait for the lock.</param>
/// <param name="token">The token that can be used to abort lock acquisition.</param>
/// <returns><see langword="true"/> if the lock is acquired;</returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="timeout"/> is negative.</exception>
/// <exception cref="ObjectDisposedException">This object has been disposed.</exception>
/// <exception cref="OperationCanceledException">The operation has been canceled.</exception>
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));
}

/// <summary>
Expand Down Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions src/DotNext.Threading/Threading/QueuedSynchronizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ private void NotifyObjectDisposed(Exception? reason = null)
lock (SyncRoot)
{
suspendedCallers = DetachWaitQueue()?.SetException(reason, out _);
Monitor.PulseAll(SyncRoot);
}

suspendedCallers?.Unwind();
Expand Down

0 comments on commit 14da392

Please sign in to comment.