Skip to content

Commit

Permalink
Preliminary support of synchronous method for taking the lock
Browse files Browse the repository at this point in the history
  • Loading branch information
sakno committed Oct 15, 2024
1 parent 834d0ba commit dce752b
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 4 deletions.
29 changes: 29 additions & 0 deletions src/DotNext.Tests/Threading/AsyncExclusiveLockTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OperationCanceledException>(() => 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();
}
}
78 changes: 74 additions & 4 deletions src/DotNext.Threading/Threading/AsyncExclusiveLock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,25 @@ public class AsyncExclusiveLock : QueuedSynchronizer, IAsyncDisposable
private struct LockManager : ILockManager<DefaultWaitNode>
{
private bool state;
internal ManualResetEventSlim? SyncState;

internal readonly bool Value => state;

internal readonly bool VolatileRead() => Volatile.Read(in state);

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<bool, DefaultWaitNode, Action<DefaultWaitNode>> pool;
Expand Down Expand Up @@ -79,20 +88,71 @@ 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)
{
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;
}

/// <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)
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
return timeout == TimeSpan.Zero ? TryAcquire() : TryAcquireCore(new(timeout), token);
}

/// <summary>
/// Tries to enter the lock in exclusive mode asynchronously, with an optional time-out.
/// </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 caller entered exclusive mode; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Time-out value is negative.</exception>
/// <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>
/// <exception cref="PendingTaskInterruptedException">The operation has been interrupted manually.</exception>
Expand All @@ -105,7 +165,7 @@ public ValueTask<bool> TryAcquireAsync(TimeSpan timeout, CancellationToken token
/// <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>The task representing lock acquisition operation.</returns>
/// <exception cref="ArgumentOutOfRangeException">Time-out value is negative.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="timeout"/> is negative.</exception>
/// <exception cref="ObjectDisposedException">This object has been disposed.</exception>
/// <exception cref="TimeoutException">The lock cannot be acquired during the specified amount of time.</exception>
/// <exception cref="OperationCanceledException">The operation has been canceled.</exception>
Expand Down Expand Up @@ -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);
}
}

0 comments on commit dce752b

Please sign in to comment.