diff --git a/AsyncKeyedLock.Tests/OriginalTests.cs b/AsyncKeyedLock.Tests/OriginalTests.cs index bfcfcc9..beef0a4 100644 --- a/AsyncKeyedLock.Tests/OriginalTests.cs +++ b/AsyncKeyedLock.Tests/OriginalTests.cs @@ -5,6 +5,38 @@ namespace AsyncKeyedLock.Tests { public class OriginalTests { + [Fact] + public async Task TestTimeout() + { + var asyncKeyedLocker = new AsyncKeyedLocker(); + using (var myFirstLock = await asyncKeyedLocker.LockAsync("test")) + { + using (var myLock = await asyncKeyedLocker.LockAsync("test", 0)) + { + var myTimeoutReleaser = (AsyncKeyedLockTimeoutReleaser)myLock; + Assert.False(myTimeoutReleaser.EnteredSemaphore); + } + Assert.True(asyncKeyedLocker.IsInUse("test")); + } + Assert.False(asyncKeyedLocker.IsInUse("test")); + } + + [Fact] + public async Task TestTimeoutWithTimeSpan() + { + var asyncKeyedLocker = new AsyncKeyedLocker(); + using (var myFirstLock = await asyncKeyedLocker.LockAsync("test")) + { + using (var myLock = await asyncKeyedLocker.LockAsync("test", TimeSpan.Zero)) + { + var myTimeoutReleaser = (AsyncKeyedLockTimeoutReleaser)myLock; + Assert.False(myTimeoutReleaser.EnteredSemaphore); + } + Assert.True(asyncKeyedLocker.IsInUse("test")); + } + Assert.False(asyncKeyedLocker.IsInUse("test")); + } + [Fact] public async Task BasicTest() { diff --git a/AsyncKeyedLock/AsyncKeyedLock.csproj b/AsyncKeyedLock/AsyncKeyedLock.csproj index 4e7c372..a3de9f9 100644 --- a/AsyncKeyedLock/AsyncKeyedLock.csproj +++ b/AsyncKeyedLock/AsyncKeyedLock.csproj @@ -8,7 +8,7 @@ https://github.com/MarkCiliaVincenti/AsyncKeyedLock MIT MIT - 6.0.0 + 6.0.1 logo.png Allowing actionable options, bug fixes with regards to timeouts, better error handling, minor optimizations, improved documentation, added considerably more tests. 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. @@ -16,8 +16,8 @@ async,lock,key,keyed,semaphore,dictionary,pooling,duplicate,synchronization git false - 6.0.0.0 - 6.0.0.0 + 6.0.1.0 + 6.0.1.0 README.md true true diff --git a/AsyncKeyedLock/AsyncKeyedLockReleaser.cs b/AsyncKeyedLock/AsyncKeyedLockReleaser.cs index cde2c22..c38a277 100644 --- a/AsyncKeyedLock/AsyncKeyedLockReleaser.cs +++ b/AsyncKeyedLock/AsyncKeyedLockReleaser.cs @@ -66,5 +66,17 @@ public void Dispose() { _dictionary.Release(this); } + + internal void Dispose(bool enteredSemaphore) + { + if (enteredSemaphore) + { + _dictionary.Release(this); + } + else + { + _dictionary.ReleaseWithoutSemaphoreRelease(this); + } + } } } \ No newline at end of file diff --git a/AsyncKeyedLock/AsyncKeyedLockTimeoutReleaser.cs b/AsyncKeyedLock/AsyncKeyedLockTimeoutReleaser.cs new file mode 100644 index 0000000..43b9689 --- /dev/null +++ b/AsyncKeyedLock/AsyncKeyedLockTimeoutReleaser.cs @@ -0,0 +1,33 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace AsyncKeyedLock +{ + /// + /// Represents an for AsyncKeyedLock with timeouts. + /// + public sealed class AsyncKeyedLockTimeoutReleaser : IDisposable + { + /// + /// True if the timeout was reached, false if not. + /// + public bool EnteredSemaphore { get; internal set; } + internal readonly AsyncKeyedLockReleaser _releaser; + + internal AsyncKeyedLockTimeoutReleaser(bool enteredSemaphore, AsyncKeyedLockReleaser releaser) + { + EnteredSemaphore = enteredSemaphore; + _releaser = releaser; + } + + /// + /// Releases the object once, depending on whether or not the semaphore was entered. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + _releaser.Dispose(EnteredSemaphore); + } + } +} \ No newline at end of file diff --git a/AsyncKeyedLock/AsyncKeyedLocker.cs b/AsyncKeyedLock/AsyncKeyedLocker.cs index 3fd2b6f..1e41b9e 100644 --- a/AsyncKeyedLock/AsyncKeyedLocker.cs +++ b/AsyncKeyedLock/AsyncKeyedLocker.cs @@ -362,6 +362,72 @@ public IDisposable Lock(TKey key, CancellationToken cancellationToken) } return releaser; } + + /// + /// Synchronously lock based on a key, setting a limit for the number of milliseconds to wait. + /// + /// The key to lock on. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// A disposable value. + public IDisposable Lock(TKey key, int millisecondsTimeout) + { + var releaser = GetOrAdd(key); + return new AsyncKeyedLockTimeoutReleaser(releaser.SemaphoreSlim.Wait(millisecondsTimeout), releaser); + } + + /// + /// Synchronously lock based on a key, setting a limit for the to wait. + /// + /// The key to lock on. + /// 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. + /// A disposable value. + public IDisposable Lock(TKey key, TimeSpan timeout) + { + var releaser = GetOrAdd(key); + return new AsyncKeyedLockTimeoutReleaser(releaser.SemaphoreSlim.Wait(timeout), releaser); + } + + /// + /// Synchronously lock based on a key, setting a limit for the number of milliseconds to wait, while observing a . + /// + /// The key to lock on. + /// 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. + /// A disposable value. + public IDisposable Lock(TKey key, int millisecondsTimeout, CancellationToken cancellationToken) + { + var releaser = GetOrAdd(key); + try + { + return new AsyncKeyedLockTimeoutReleaser(releaser.SemaphoreSlim.Wait(millisecondsTimeout, cancellationToken), releaser); + } + catch (OperationCanceledException) + { + Release(releaser); + throw; + } + } + + /// + /// Synchronously lock based on a key, setting a limit for the to wait, while observing a . + /// + /// The key to lock on. + /// 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. + /// A disposable value. + public IDisposable Lock(TKey key, TimeSpan timeout, CancellationToken cancellationToken) + { + var releaser = GetOrAdd(key); + try + { + return new AsyncKeyedLockTimeoutReleaser(releaser.SemaphoreSlim.Wait(timeout, cancellationToken), releaser); + } + catch (OperationCanceledException) + { + Release(releaser); + throw; + } + } #endregion Synchronous #region SynchronousTry @@ -779,6 +845,72 @@ public async ValueTask LockAsync(TKey key, CancellationToken cancel } return releaser; } + + /// + /// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait. + /// + /// The key to lock on. + /// The number of milliseconds to wait, (-1) to wait indefinitely, or zero to test the state of the wait handle and return immediately. + /// A disposable value. + public async ValueTask LockAsync(TKey key, int millisecondsTimeout) + { + var releaser = GetOrAdd(key); + return new AsyncKeyedLockTimeoutReleaser(await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout).ConfigureAwait(false), releaser); + } + + /// + /// Asynchronously lock based on a key, setting a limit for the to wait. + /// + /// The key to lock on. + /// 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. + /// A disposable value. + public async ValueTask LockAsync(TKey key, TimeSpan timeout) + { + var releaser = GetOrAdd(key); + return new AsyncKeyedLockTimeoutReleaser(await releaser.SemaphoreSlim.WaitAsync(timeout).ConfigureAwait(false), releaser); + } + + /// + /// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait, while observing a . + /// + /// The key to lock on. + /// 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. + /// A disposable value. + public async ValueTask LockAsync(TKey key, int millisecondsTimeout, CancellationToken cancellationToken) + { + var releaser = GetOrAdd(key); + try + { + return new AsyncKeyedLockTimeoutReleaser(await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout, cancellationToken).ConfigureAwait(false), releaser); + } + catch (OperationCanceledException) + { + Release(releaser); + throw; + } + } + + /// + /// Asynchronously lock based on a key, setting a limit for the to wait, while observing a . + /// + /// The key to lock on. + /// 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. + /// A disposable value. + public async ValueTask LockAsync(TKey key, TimeSpan timeout, CancellationToken cancellationToken) + { + var releaser = GetOrAdd(key); + try + { + return new AsyncKeyedLockTimeoutReleaser(await releaser.SemaphoreSlim.WaitAsync(timeout, cancellationToken).ConfigureAwait(false), releaser); + } + catch (OperationCanceledException) + { + Release(releaser); + throw; + } + } #endregion /// diff --git a/README.md b/README.md index 790852a..1dde400 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,9 @@ using (var lockObj = await asyncKeyedLocker.LockAsync(myObject, cancellationToke } ``` -In the case you need to use timeouts, you can also use `TryLockAsync` methods which will call a `Func` or `Action` if the timeout is not expired, whilst returning a boolean representing whether or not it waited successfully. +You can also use timeouts with overloaded methods to set the maximum time to wait, either in milliseconds or as a `TimeSpan`. + +In the case you need to use timeouts to instead give up if unable to obtain a lock by a certain amount of time, you can also use `TryLockAsync` methods which will call a `Func` or `Action` if the timeout is not expired, whilst returning a boolean representing whether or not it waited successfully. There are also synchronous `Lock` and `TryLock` methods available.