Skip to content

Commit

Permalink
Added synchronous reader/writer lock acquisition support
Browse files Browse the repository at this point in the history
  • Loading branch information
sakno committed Oct 15, 2024
1 parent 14da392 commit 0252f0f
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 33 deletions.
68 changes: 67 additions & 1 deletion src/DotNext.Tests/Threading/AsyncReaderWriterLockTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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<ObjectDisposedException>(() => 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<ObjectDisposedException>(() => 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);
}
}
27 changes: 2 additions & 25 deletions src/DotNext.Threading/Threading/AsyncExclusiveLock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
/// Tries to acquire the lock synchronously.
/// </summary>
/// <param name="timeout">The interval to wait for the lock.</param>
/// <returns><see langword="true"/> if the lock is acquired;</returns>
/// <returns><see langword="true"/> if the lock is acquired in timely manner; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="timeout"/> is negative.</exception>
/// <exception cref="ObjectDisposedException">This object has been disposed.</exception>
[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);
}

/// <summary>
Expand Down
66 changes: 59 additions & 7 deletions src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace DotNext.Threading;

Expand Down Expand Up @@ -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;
Expand All @@ -62,6 +64,8 @@ internal void ExitLock()
{
readLocks--;
}

return result;
}

internal readonly long ReadLocks => Volatile.Read(in readLocks);
Expand Down Expand Up @@ -276,11 +280,29 @@ public LockStamp TryOptimisticRead()
public bool Validate(in LockStamp stamp) => stamp.IsValid(in state);

/// <summary>
/// Attempts to obtain reader lock synchronously without blocking caller thread.
/// Tries to obtain reader lock synchronously without blocking caller thread.
/// </summary>
/// <returns><see langword="true"/> if lock is taken successfuly; otherwise, <see langword="false"/>.</returns>
/// <returns><see langword="true"/> if lock is taken successfully; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ObjectDisposedException">This object has been disposed.</exception>
public bool TryEnterReadLock() => TryEnter<ReadLockManager>();

/// <summary>
/// Tries to obtain reader lock synchronously.
/// </summary>
/// <param name="timeout">The time to wait.</param>
/// <returns><see langword="true"/> if reader lock is acquired in timely manner; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="timeout"/> is negative.</exception>
/// <exception cref="ObjectDisposedException">This object has been disposed.</exception>
[UnsupportedOSPlatform("browser")]
public bool TryEnterReadLock(TimeSpan timeout)
{
ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this);
return TryEnter<ReadLockManager>(timeout);
}

private bool TryEnter<TLockManager>(TimeSpan timeout)
where TLockManager : struct, ILockManager<WaitNode>
=> timeout == TimeSpan.Zero ? TryEnter<TLockManager>() : TryAcquire(new Timeout(timeout), ref GetLockManager<TLockManager>());

/// <summary>
/// Tries to enter the lock in read mode asynchronously, with an optional time-out.
Expand Down Expand Up @@ -341,9 +363,23 @@ public bool TryEnterWriteLock(in LockStamp stamp)
/// <summary>
/// Attempts to obtain writer lock synchronously without blocking caller thread.
/// </summary>
/// <returns><see langword="true"/> if lock is taken successfuly; otherwise, <see langword="false"/>.</returns>
/// <returns><see langword="true"/> if lock is taken successfully; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ObjectDisposedException">This object has been disposed.</exception>
public bool TryEnterWriteLock() => TryEnter<WriteLockManager>();

/// <summary>
/// Tries to obtain writer lock synchronously.
/// </summary>
/// <param name="timeout">The time to wait.</param>
/// <returns><see langword="true"/> if writer lock is acquired in timely manner; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="timeout"/> is negative.</exception>
/// <exception cref="ObjectDisposedException">This object has been disposed.</exception>
[UnsupportedOSPlatform("browser")]
public bool TryEnterWriteLock(TimeSpan timeout)
{
ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this);
return TryEnter<WriteLockManager>(timeout);
}

/// <summary>
/// Tries to enter the lock in write mode asynchronously, with an optional time-out.
Expand Down Expand Up @@ -387,7 +423,7 @@ public ValueTask EnterWriteLockAsync(TimeSpan timeout, CancellationToken token =
/// <summary>
/// Tries to upgrade the read lock to the write lock synchronously without blocking of the caller.
/// </summary>
/// <returns><see langword="true"/> if lock is taken successfuly; otherwise, <see langword="false"/>.</returns>
/// <returns><see langword="true"/> if lock is taken successfully; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ObjectDisposedException">This object has been disposed.</exception>
public bool TryUpgradeToWriteLock() => TryEnter<UpgradeManager>();

Expand Down Expand Up @@ -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();
Expand All @@ -591,6 +640,9 @@ public void DowngradeFromWriteLock()

state.DowngradeFromWriteLock();
suspendedCallers = DrainWaitQueue();

// resume multiple readers if available
Monitor.PulseAll(SyncRoot);
}

suspendedCallers?.Unwind();
Expand Down
27 changes: 27 additions & 0 deletions src/DotNext.Threading/Threading/QueuedSynchronizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Diagnostics.Metrics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace DotNext.Threading;

Expand Down Expand Up @@ -169,6 +170,32 @@ private protected bool TryAcquire<TLockManager>(ref TLockManager manager)
return true;
}

[UnsupportedOSPlatform("browser")]
private protected bool TryAcquire<TLockManager>(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<TLockManager>(ref TLockManager manager)
where TLockManager : struct, ILockManager
{
ObjectDisposedException.ThrowIf(IsDisposingOrDisposed, this);
return TryAcquire(ref manager);
}

private T AcquireAsync<T, TNode, TInitializer, TLockManager, TOptions>(ref ValueTaskPool<bool, TNode, Action<TNode>> pool, ref TLockManager manager, TInitializer initializer, TOptions options)
where T : struct, IEquatable<T>
where TNode : WaitNode, IValueTaskFactory<T>, IPooledManualResetCompletionSource<Action<TNode>>, new()
Expand Down

0 comments on commit 0252f0f

Please sign in to comment.