diff --git a/AsyncKeyedLock.Tests/AsyncNonKeyedLockerTests/OriginalTests.cs b/AsyncKeyedLock.Tests/AsyncNonKeyedLockerTests/OriginalTests.cs index c908d43..8b653a2 100644 --- a/AsyncKeyedLock.Tests/AsyncNonKeyedLockerTests/OriginalTests.cs +++ b/AsyncKeyedLock.Tests/AsyncNonKeyedLockerTests/OriginalTests.cs @@ -5,6 +5,222 @@ namespace AsyncKeyedLock.Tests.AsyncNonKeyedLockerTests { public class OriginalTests { + [Fact] + public void TestRecursion() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncNonKeyedLocker.ConditionalLock(isFirst)) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionAsync() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncNonKeyedLocker.ConditionalLockAsync(isFirst)) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + + [Fact] + public void TestRecursionWithCancellationToken() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncNonKeyedLocker.ConditionalLock(isFirst, new CancellationToken(false))) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionWithCancellationTokenAsync() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncNonKeyedLocker.ConditionalLockAsync(isFirst, new CancellationToken(false))) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + + [Fact] + public void TestRecursionWithTimeout() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncNonKeyedLocker.ConditionalLock(isFirst, Timeout.Infinite, out _)) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionWithTimeoutAsync() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncNonKeyedLocker.ConditionalLockAsync(isFirst, Timeout.Infinite)) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + + [Fact] + public void TestRecursionWithTimeSpan() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncNonKeyedLocker.ConditionalLock(isFirst, TimeSpan.Zero, out _)) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionWithTimeSpanAsync() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncNonKeyedLocker.ConditionalLockAsync(isFirst, TimeSpan.Zero)) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + + [Fact] + public void TestRecursionWithTimeoutAndCancellationToken() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncNonKeyedLocker.ConditionalLock(isFirst, Timeout.Infinite, new CancellationToken(false), out _)) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionWithTimeoutAndCancellationTokenAsync() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncNonKeyedLocker.ConditionalLockAsync(isFirst, Timeout.Infinite, new CancellationToken(false))) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + + [Fact] + public void TestRecursionWithTimeSpanAndCancellationToken() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncNonKeyedLocker.ConditionalLock(isFirst, TimeSpan.Zero, new CancellationToken(false), out _)) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionWithTimeSpanAndCancellationTokenAsync() + { + var asyncNonKeyedLocker = new AsyncNonKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncNonKeyedLocker.ConditionalLockAsync(isFirst, TimeSpan.Zero, new CancellationToken(false))) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + [Fact] public void TestMaxCount() { diff --git a/AsyncKeyedLock.Tests/StripedAsyncKeyedLocker/OriginalTests.cs b/AsyncKeyedLock.Tests/StripedAsyncKeyedLocker/OriginalTests.cs index 6ae3952..23df23f 100644 --- a/AsyncKeyedLock.Tests/StripedAsyncKeyedLocker/OriginalTests.cs +++ b/AsyncKeyedLock.Tests/StripedAsyncKeyedLocker/OriginalTests.cs @@ -9,6 +9,222 @@ namespace AsyncKeyedLock.Tests.StripedAsyncKeyedLocker { public class OriginalTests { + [Fact] + public void TestRecursion() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncKeyedLocker.ConditionalLock("test123", isFirst)) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionAsync() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncKeyedLocker.ConditionalLockAsync("test123", isFirst)) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + + [Fact] + public void TestRecursionWithCancellationToken() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncKeyedLocker.ConditionalLock("test123", isFirst, new CancellationToken(false))) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionWithCancellationTokenAsync() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncKeyedLocker.ConditionalLockAsync("test123", isFirst, new CancellationToken(false))) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + + [Fact] + public void TestRecursionWithTimeout() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncKeyedLocker.ConditionalLock("test123", isFirst, Timeout.Infinite, out _)) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionWithTimeoutAsync() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncKeyedLocker.ConditionalLockAsync("test123", isFirst, Timeout.Infinite)) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + + [Fact] + public void TestRecursionWithTimeSpan() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncKeyedLocker.ConditionalLock("test123", isFirst, TimeSpan.Zero, out _)) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionWithTimeSpanAsync() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncKeyedLocker.ConditionalLockAsync("test123", isFirst, TimeSpan.Zero)) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + + [Fact] + public void TestRecursionWithTimeoutAndCancellationToken() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncKeyedLocker.ConditionalLock("test123", isFirst, Timeout.Infinite, new CancellationToken(false), out _)) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionWithTimeoutAndCancellationTokenAsync() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncKeyedLocker.ConditionalLockAsync("test123", isFirst, Timeout.Infinite, new CancellationToken(false))) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + + [Fact] + public void TestRecursionWithTimeSpanAndCancellationToken() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + double Factorial(int number, bool isFirst = true) + { + using (asyncKeyedLocker.ConditionalLock("test123", isFirst, TimeSpan.Zero, new CancellationToken(false), out _)) + { + if (number == 0) + return 1; + return number * Factorial(number - 1, false); + } + } + + Assert.Equal(120, Factorial(5)); + } + + [Fact] + public async Task TestRecursionWithTimeSpanAndCancellationTokenAsync() + { + var asyncKeyedLocker = new StripedAsyncKeyedLocker(); + + async Task Factorial(int number, bool isFirst = true) + { + using (await asyncKeyedLocker.ConditionalLockAsync("test123", isFirst, TimeSpan.Zero, new CancellationToken(false))) + { + if (number == 0) + return 1; + return number * await Factorial(number - 1, false); + } + } + + Assert.Equal(120, await Factorial(5)); + } + [Fact] public void TestHashHelpersIsPrime0DoesNotThrow() { diff --git a/AsyncKeyedLock/AsyncKeyedLock.csproj b/AsyncKeyedLock/AsyncKeyedLock.csproj index ed16937..5aa9b70 100644 --- a/AsyncKeyedLock/AsyncKeyedLock.csproj +++ b/AsyncKeyedLock/AsyncKeyedLock.csproj @@ -8,16 +8,16 @@ https://github.com/MarkCiliaVincenti/AsyncKeyedLock MIT MIT - 6.4.0 + 6.4.1 logo.png - New methods for conditional locking which can be used in recursive methods as a workaround for reentrancy. If the condition is false, it enters without locking. + New methods for conditional locking enabled also for StripedAsyncKeyedLocker and AsyncNonKeyedLocker. An asynchronous .NET Standard 2.0 library that allows you to lock based on a key (keyed semaphores), limiting concurrent threads sharing the same key to a specified number, with optional pooling for reducing memory allocations. © 2024 Mark Cilia Vincenti async,lock,key,keyed,semaphore,striped,dictionary,concurrentdictionary,pooling,duplicate,synchronization git false - 6.4.0.0 - 6.4.0.0 + 6.4.1.0 + 6.4.1.0 README.md true True diff --git a/AsyncKeyedLock/AsyncNonKeyedLockReleaser.cs b/AsyncKeyedLock/AsyncNonKeyedLockReleaser.cs index 22c8e13..505c993 100644 --- a/AsyncKeyedLock/AsyncNonKeyedLockReleaser.cs +++ b/AsyncKeyedLock/AsyncNonKeyedLockReleaser.cs @@ -22,7 +22,7 @@ internal AsyncNonKeyedLockReleaser(AsyncNonKeyedLocker locker) [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly void Dispose() { - _locker._semaphoreSlim.Release(); + _locker?._semaphoreSlim.Release(); } } } \ No newline at end of file diff --git a/AsyncKeyedLock/AsyncNonKeyedLocker.cs b/AsyncKeyedLock/AsyncNonKeyedLocker.cs index d4e76f8..17906c7 100644 --- a/AsyncKeyedLock/AsyncNonKeyedLocker.cs +++ b/AsyncKeyedLock/AsyncNonKeyedLocker.cs @@ -344,6 +344,335 @@ public async ValueTask LockAsync(TimeSpan time #endif #endregion AsynchronousNet8.0 + #region ConditionalSynchronous + /// + /// Synchronously lock. If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public AsyncNonKeyedLockReleaser ConditionalLock(bool getLock) + { + if (!getLock) + { + return new AsyncNonKeyedLockReleaser(null); + } + return Lock(); + } + + /// + /// Synchronously lock, while observing a . If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// The to observe. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public AsyncNonKeyedLockReleaser ConditionalLock(bool getLock, CancellationToken cancellationToken) + { + if (!getLock) + { + return new AsyncNonKeyedLockReleaser(null); + } + return Lock(cancellationToken); + } + + /// + /// Synchronously lock, setting a limit for the number of milliseconds to wait. If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// An out parameter showing whether or not the semaphore was entered. + /// A disposable value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDisposable ConditionalLock(bool getLock, int millisecondsTimeout, out bool entered) + { + if (!getLock) + { + entered = false; + return new AsyncNonKeyedLockTimeoutReleaser(null, false); + } + return Lock(millisecondsTimeout, out entered); + } + + /// + /// Synchronously lock, setting a limit for the to wait. If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// An out parameter showing whether or not the semaphore was entered. + /// A disposable value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDisposable ConditionalLock(bool getLock, TimeSpan timeout, out bool entered) + { + if (!getLock) + { + entered = false; + return new AsyncNonKeyedLockTimeoutReleaser(null, false); + } + return Lock(timeout, out entered); + } + + /// + /// Synchronously lock, setting a limit for the number of milliseconds to wait, while observing a . If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// The to observe. + /// An out parameter showing whether or not the semaphore was entered. + /// A disposable value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDisposable ConditionalLock(bool getLock, int millisecondsTimeout, CancellationToken cancellationToken, out bool entered) + { + if (!getLock) + { + entered = false; + return new AsyncNonKeyedLockTimeoutReleaser(null, false); + } + return Lock(millisecondsTimeout, cancellationToken, out entered); + } + + /// + /// Synchronously lock, setting a limit for the to wait, while observing a . If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// The to observe. + /// An out parameter showing whether or not the semaphore was entered. + /// A disposable value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDisposable ConditionalLock(bool getLock, TimeSpan timeout, CancellationToken cancellationToken, out bool entered) + { + if (!getLock) + { + entered = false; + return new AsyncNonKeyedLockTimeoutReleaser(null, false); + } + return Lock(timeout, cancellationToken, out entered); + } + #endregion ConditionalSynchronous + + #region ConditionalAsynchronous + /// + /// Asynchronously lock. If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, bool continueOnCapturedContext = false) + { + if (!getLock) + { + return new AsyncNonKeyedLockReleaser(null); + } + return await LockAsync(continueOnCapturedContext).ConfigureAwait(continueOnCapturedContext); + } + + /// + /// Asynchronously lock, while observing a . If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// The to observe. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, CancellationToken cancellationToken, bool continueOnCapturedContext = false) + { + if (!getLock) + { + return new AsyncNonKeyedLockReleaser(null); + } + return await LockAsync(cancellationToken, continueOnCapturedContext).ConfigureAwait(continueOnCapturedContext); + } + + /// + /// Asynchronously lock, setting a limit for the number of milliseconds to wait. If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, int millisecondsTimeout, bool continueOnCapturedContext = false) + { + bool entered = getLock && await _semaphoreSlim.WaitAsync(millisecondsTimeout).ConfigureAwait(continueOnCapturedContext); + return new AsyncNonKeyedLockTimeoutReleaser(this, entered); + } + + /// + /// Asynchronously lock, setting a limit for the to wait. If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, TimeSpan timeout, bool continueOnCapturedContext = false) + { + bool entered = getLock && await _semaphoreSlim.WaitAsync(timeout).ConfigureAwait(continueOnCapturedContext); + return new AsyncNonKeyedLockTimeoutReleaser(this, entered); + } + + /// + /// Asynchronously lock, setting a limit for the number of milliseconds to wait, while observing a . If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// The to observe. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, int millisecondsTimeout, CancellationToken cancellationToken, bool continueOnCapturedContext = false) + { + try + { + bool entered = getLock && await _semaphoreSlim.WaitAsync(millisecondsTimeout, cancellationToken).ConfigureAwait(continueOnCapturedContext); + return new AsyncNonKeyedLockTimeoutReleaser(this, entered); + } + catch (OperationCanceledException) + { + return new AsyncNonKeyedLockTimeoutReleaser(this, false); + throw; + } + } + + /// + /// Asynchronously lock, setting a limit for the to wait, while observing a . If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// The to observe. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, TimeSpan timeout, CancellationToken cancellationToken, bool continueOnCapturedContext = false) + { + try + { + bool entered = getLock && await _semaphoreSlim.WaitAsync(timeout, cancellationToken).ConfigureAwait(continueOnCapturedContext); + return new AsyncNonKeyedLockTimeoutReleaser(this, entered); + } + catch (OperationCanceledException) + { + return new AsyncNonKeyedLockTimeoutReleaser(this, false); + throw; + } + } + #endregion ConditionalAsynchronous + + #region ConditionalAsynchronousNet8.0 +#if NET8_0_OR_GREATER + /// + /// Asynchronously lock. If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// Options used to configure how awaits on this task are performed. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, ConfigureAwaitOptions configureAwaitOptions) + { + if (!getLock) + { + return new AsyncNonKeyedLockReleaser(null); + } + await _semaphoreSlim.WaitAsync().ConfigureAwait(configureAwaitOptions); + return new AsyncNonKeyedLockReleaser(this); + } + + /// + /// Asynchronously lock, while observing a . If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// The to observe. + /// Options used to configure how awaits on this task are performed. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, CancellationToken cancellationToken, ConfigureAwaitOptions configureAwaitOptions) + { + if (!getLock) + { + return new AsyncNonKeyedLockReleaser(null); + } + await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(configureAwaitOptions); + return new AsyncNonKeyedLockReleaser(this); + } + + /// + /// Asynchronously lock, setting a limit for the number of milliseconds to wait. If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// Options used to configure how awaits on this task are performed. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, int millisecondsTimeout, ConfigureAwaitOptions configureAwaitOptions) + { + bool entered = getLock && await _semaphoreSlim.WaitAsync(millisecondsTimeout).ConfigureAwait(configureAwaitOptions); + return new AsyncNonKeyedLockTimeoutReleaser(this, entered); + } + + /// + /// Asynchronously lock, setting a limit for the to wait. If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// Options used to configure how awaits on this task are performed. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, TimeSpan timeout, ConfigureAwaitOptions configureAwaitOptions) + { + bool entered = getLock && await _semaphoreSlim.WaitAsync(timeout).ConfigureAwait(configureAwaitOptions); + return new AsyncNonKeyedLockTimeoutReleaser(this, entered); + } + + /// + /// Asynchronously lock, setting a limit for the number of milliseconds to wait, while observing a . If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// The to observe. + /// Options used to configure how awaits on this task are performed. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, int millisecondsTimeout, CancellationToken cancellationToken, ConfigureAwaitOptions configureAwaitOptions) + { + try + { + bool entered = getLock && await _semaphoreSlim.WaitAsync(millisecondsTimeout, cancellationToken).ConfigureAwait(configureAwaitOptions); + return new AsyncNonKeyedLockTimeoutReleaser(this, entered); + } + catch (OperationCanceledException) + { + return new AsyncNonKeyedLockTimeoutReleaser(this, false); + throw; + } + } + + /// + /// Asynchronously lock, setting a limit for the to wait, while observing a . If the condition is false, it enters without locking. + /// + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// The to observe. + /// Options used to configure how awaits on this task are performed. + /// A disposable value of type . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async ValueTask ConditionalLockAsync(bool getLock, TimeSpan timeout, CancellationToken cancellationToken, ConfigureAwaitOptions configureAwaitOptions) + { + try + { + bool entered = getLock && await _semaphoreSlim.WaitAsync(timeout, cancellationToken).ConfigureAwait(configureAwaitOptions); + return new AsyncNonKeyedLockTimeoutReleaser(this, entered); + } + catch (OperationCanceledException) + { + return new AsyncNonKeyedLockTimeoutReleaser(this, false); + throw; + } + } +#endif + #endregion ConditionalAsynchronousNet8.0 + /// /// Get the number of requests concurrently locked. /// diff --git a/AsyncKeyedLock/StripedAsyncKeyedLocker.cs b/AsyncKeyedLock/StripedAsyncKeyedLocker.cs index 68ea158..c6da061 100644 --- a/AsyncKeyedLock/StripedAsyncKeyedLocker.cs +++ b/AsyncKeyedLock/StripedAsyncKeyedLocker.cs @@ -917,6 +917,305 @@ public async ValueTask LockAsync(TKey key, #endif #endregion AsynchronousNet8.0 + #region ConditionalSynchronous + /// + /// Synchronously lock based on a key. If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// A disposable value. + public IDisposable ConditionalLock(TKey key, bool getLock) + { + if (!getLock) + { + return _emptyDisposable; + } + return Lock(key); + } + + /// + /// Synchronously lock based on a key, while observing a . If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// The to observe. + /// A disposable value. + public IDisposable ConditionalLock(TKey key, bool getLock, CancellationToken cancellationToken) + { + if (!getLock) + { + return _emptyDisposable; + } + return Lock(key, cancellationToken); + } + + /// + /// Synchronously lock based on a key, setting a limit for the number of milliseconds to wait. If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// An out parameter showing whether or not the semaphore was entered. + /// A disposable value. + public IDisposable ConditionalLock(TKey key, bool getLock, int millisecondsTimeout, out bool entered) + { + if (!getLock) + { + entered = false; + return _emptyDisposable; + } + return Lock(key, millisecondsTimeout, out entered); + } + + /// + /// Synchronously lock based on a key, setting a limit for the to wait. If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// An out parameter showing whether or not the semaphore was entered. + /// A disposable value. + public IDisposable ConditionalLock(TKey key, bool getLock, TimeSpan timeout, out bool entered) + { + if (!getLock) + { + entered = false; + return _emptyDisposable; + } + return Lock(key, timeout, out entered); + } + + /// + /// Synchronously lock based on a key, setting a limit for the number of milliseconds to wait, while observing a . If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// The to observe. + /// An out parameter showing whether or not the semaphore was entered. + /// A disposable value. + public IDisposable ConditionalLock(TKey key, bool getLock, int millisecondsTimeout, CancellationToken cancellationToken, out bool entered) + { + if (!getLock) + { + entered = false; + return _emptyDisposable; + } + return Lock(key, millisecondsTimeout, cancellationToken, out entered); + } + + /// + /// Synchronously lock based on a key, setting a limit for the to wait, while observing a . If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// The to observe. + /// An out parameter showing whether or not the semaphore was entered. + /// A disposable value. + public IDisposable ConditionalLock(TKey key, bool getLock, TimeSpan timeout, CancellationToken cancellationToken, out bool entered) + { + if (!getLock) + { + entered = false; + return _emptyDisposable; + } + return Lock(key, timeout, cancellationToken, out entered); + } + #endregion ConditionalSynchronous + + #region ConditionalAsynchronous + /// + /// Asynchronously lock based on a key. If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value. + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, bool continueOnCapturedContext = false) + { + if (!getLock) + { + return new StripedAsyncKeyedLockReleaser { SemaphoreSlim = new SemaphoreSlim(0, 1) }; + } + return await LockAsync(key, continueOnCapturedContext).ConfigureAwait(continueOnCapturedContext); + } + + /// + /// Asynchronously lock based on a key, while observing a . If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// The to observe. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value. + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, CancellationToken cancellationToken, bool continueOnCapturedContext = false) + { + if (!getLock) + { + return new StripedAsyncKeyedLockReleaser { SemaphoreSlim = new SemaphoreSlim(0, 1) }; + } + return await LockAsync(key, cancellationToken, continueOnCapturedContext).ConfigureAwait(continueOnCapturedContext); + } + + /// + /// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait. If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value of type . + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, int millisecondsTimeout, bool continueOnCapturedContext = false) + { + var releaser = Get(key); + return new StripedAsyncKeyedLockTimeoutReleaser(getLock && await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout).ConfigureAwait(continueOnCapturedContext), releaser); + } + + /// + /// Asynchronously lock based on a key, setting a limit for the to wait. If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value of type . + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, TimeSpan timeout, bool continueOnCapturedContext = false) + { + var releaser = Get(key); + return new StripedAsyncKeyedLockTimeoutReleaser(getLock && await releaser.SemaphoreSlim.WaitAsync(timeout).ConfigureAwait(continueOnCapturedContext), releaser); + } + + /// + /// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait, while observing a . If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// The to observe. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value of type . + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, int millisecondsTimeout, CancellationToken cancellationToken, bool continueOnCapturedContext = false) + { + var releaser = Get(key); + return new StripedAsyncKeyedLockTimeoutReleaser(getLock && await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout, cancellationToken).ConfigureAwait(continueOnCapturedContext), releaser); + } + + /// + /// Asynchronously lock based on a key, setting a limit for the to wait, while observing a . If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// The to observe. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. Defaults to false. + /// A disposable value of type . + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, TimeSpan timeout, CancellationToken cancellationToken, bool continueOnCapturedContext = false) + { + var releaser = Get(key); + return new StripedAsyncKeyedLockTimeoutReleaser(getLock && await releaser.SemaphoreSlim.WaitAsync(timeout, cancellationToken).ConfigureAwait(continueOnCapturedContext), releaser); + } + #endregion ConditionalAsynchronous + + #region ConditionalAsynchronousNet8.0 +#if NET8_0_OR_GREATER + /// + /// Asynchronously lock based on a key. If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// Options used to configure how awaits on this task are performed. + /// A disposable value. + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, ConfigureAwaitOptions configureAwaitOptions) + { + if (!getLock) + { + return new StripedAsyncKeyedLockReleaser { SemaphoreSlim = new SemaphoreSlim(0, 1) }; + } + var releaser = Get(key); + await releaser.SemaphoreSlim.WaitAsync().ConfigureAwait(configureAwaitOptions); + return releaser; + } + + /// + /// Asynchronously lock based on a key, while observing a . If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// The to observe. + /// Options used to configure how awaits on this task are performed. + /// A disposable value. + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, CancellationToken cancellationToken, ConfigureAwaitOptions configureAwaitOptions) + { + if (!getLock) + { + return new StripedAsyncKeyedLockReleaser { SemaphoreSlim = new SemaphoreSlim(0, 1) }; + } + var releaser = Get(key); + await releaser.SemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(configureAwaitOptions); + return releaser; + } + + /// + /// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait. If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// Options used to configure how awaits on this task are performed. + /// A disposable value of type . + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, int millisecondsTimeout, ConfigureAwaitOptions configureAwaitOptions) + { + var releaser = Get(key); + return new StripedAsyncKeyedLockTimeoutReleaser(getLock && await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout).ConfigureAwait(configureAwaitOptions), releaser); + } + + /// + /// Asynchronously lock based on a key, setting a limit for the to wait. If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// Options used to configure how awaits on this task are performed. + /// A disposable value of type . + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, TimeSpan timeout, ConfigureAwaitOptions configureAwaitOptions) + { + var releaser = Get(key); + return new StripedAsyncKeyedLockTimeoutReleaser(getLock && await releaser.SemaphoreSlim.WaitAsync(timeout).ConfigureAwait(configureAwaitOptions), releaser); + } + + /// + /// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait, while observing a . If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// The to observe. + /// Options used to configure how awaits on this task are performed. + /// A disposable value of type . + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, int millisecondsTimeout, CancellationToken cancellationToken, ConfigureAwaitOptions configureAwaitOptions) + { + var releaser = Get(key); + return new StripedAsyncKeyedLockTimeoutReleaser(getLock && await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout, cancellationToken).ConfigureAwait(configureAwaitOptions), releaser); + } + + /// + /// Asynchronously lock based on a key, setting a limit for the to wait, while observing a . If the condition is false, it enters without locking. + /// + /// The key to lock on. + /// Condition for getting lock if true, otherwise enters without locking. + /// A that represents the number of milliseconds to wait, a that represents -1 milliseconds to wait indefinitely, or a that represents 0 milliseconds to test the wait handle and return immediately. + /// The to observe. + /// Options used to configure how awaits on this task are performed. + /// A disposable value of type . + public async ValueTask ConditionalLockAsync(TKey key, bool getLock, TimeSpan timeout, CancellationToken cancellationToken, ConfigureAwaitOptions configureAwaitOptions) + { + var releaser = Get(key); + return new StripedAsyncKeyedLockTimeoutReleaser(getLock && await releaser.SemaphoreSlim.WaitAsync(timeout, cancellationToken).ConfigureAwait(configureAwaitOptions), releaser); + } +#endif + #endregion ConditionalAsynchronousNet8.0 + /// /// Checks whether or not there is a thread making use of a keyed lock. Since striped locking means some keys could share the same lock, /// a value of true does not necessarily mean that the key is in use but that its lock is in use. diff --git a/README.md b/README.md index 0bc313a..2dec046 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ using (await _asyncKeyedLocker.LockAsync("test123")) } ``` -This libary also supports conditional locking. This could provide a workaround for `reentrancy` in some scenarios for example in recursion: +This libary also supports conditional locking, whether for `AsyncKeyedLocker`, `StripedAsyncKeyedLocker` or `AsyncNonKeyedLocker`. This could provide a workaround for `reentrancy` in some scenarios for example in recursion: ```csharp double factorial = Factorial(number);