From f89c81d15cc10ddcf40b60d0c29711481bf6a38f Mon Sep 17 00:00:00 2001 From: Mark Cilia Vincenti Date: Sat, 17 Dec 2022 19:00:30 +0100 Subject: [PATCH] Allowing actionable options, bug fixes with regards to timeouts, better error handling, minor optimizations, improved documentation, added considerably more tests. --- .../AsyncKeyedLock.Tests.csproj | 1 + .../{Tests.cs => OriginalTests.cs} | 76 ++++- .../TestsForAsyncKeyedLock.cs | 225 +++++++++++++ .../TestsForAsyncKeyedLockDictionary.cs | 226 +++++++++++++ .../TestsForCancellationAndTimeout.cs | 308 ++++++++++++++++++ AsyncKeyedLock/AsyncKeyedLock.csproj | 10 +- AsyncKeyedLock/AsyncKeyedLockDictionary.cs | 19 ++ AsyncKeyedLock/AsyncKeyedLocker.cs | 298 ++++++----------- README.md | 31 +- 9 files changed, 973 insertions(+), 221 deletions(-) rename AsyncKeyedLock.Tests/{Tests.cs => OriginalTests.cs} (85%) create mode 100644 AsyncKeyedLock.Tests/TestsForAsyncKeyedLock.cs create mode 100644 AsyncKeyedLock.Tests/TestsForAsyncKeyedLockDictionary.cs create mode 100644 AsyncKeyedLock.Tests/TestsForCancellationAndTimeout.cs diff --git a/AsyncKeyedLock.Tests/AsyncKeyedLock.Tests.csproj b/AsyncKeyedLock.Tests/AsyncKeyedLock.Tests.csproj index d78498e..0cd56f0 100644 --- a/AsyncKeyedLock.Tests/AsyncKeyedLock.Tests.csproj +++ b/AsyncKeyedLock.Tests/AsyncKeyedLock.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/AsyncKeyedLock.Tests/Tests.cs b/AsyncKeyedLock.Tests/OriginalTests.cs similarity index 85% rename from AsyncKeyedLock.Tests/Tests.cs rename to AsyncKeyedLock.Tests/OriginalTests.cs index d400d3b..bfcfcc9 100644 --- a/AsyncKeyedLock.Tests/Tests.cs +++ b/AsyncKeyedLock.Tests/OriginalTests.cs @@ -3,7 +3,7 @@ namespace AsyncKeyedLock.Tests { - public class Tests + public class OriginalTests { [Fact] public async Task BasicTest() @@ -19,8 +19,9 @@ public async Task BasicTest() var key = Convert.ToInt32(Math.Ceiling((double)i / concurrency)); using (await asyncKeyedLocker.LockAsync(key)) { + await Task.Delay(20); concurrentQueue.Enqueue((true, key)); - await Task.Delay(100); + await Task.Delay(80); concurrentQueue.Enqueue((false, key)); } }); @@ -70,8 +71,9 @@ public async Task BasicTestGenerics() var key = Convert.ToInt32(Math.Ceiling((double)i / concurrency)); using (await asyncKeyedLocker.LockAsync(key)) { + await Task.Delay(20); concurrentQueue.Enqueue((true, key)); - await Task.Delay(10); + await Task.Delay(80); concurrentQueue.Enqueue((false, key)); } }); @@ -121,8 +123,65 @@ public async Task BasicTestGenericsPooling50k() var key = Convert.ToInt32(Math.Ceiling((double)i / concurrency)); using (await asyncKeyedLocker.LockAsync(key)) { + await Task.Delay(20); concurrentQueue.Enqueue((true, key)); - await Task.Delay(10); + await Task.Delay(80); + concurrentQueue.Enqueue((false, key)); + } + }); + await Task.WhenAll(tasks.AsParallel()); + + bool valid = concurrentQueue.Count == locks * concurrency * 2; + + var entered = new HashSet(); + + while (valid && !concurrentQueue.IsEmpty) + { + concurrentQueue.TryDequeue(out var result); + if (result.entered) + { + if (entered.Contains(result.key)) + { + valid = false; + break; + } + entered.Add(result.key); + } + else + { + if (!entered.Contains(result.key)) + { + valid = false; + break; + } + entered.Remove(result.key); + } + } + + Assert.True(valid); + } + + [Fact] + public async Task BasicTestGenericsPooling50kUnfilled() + { + var locks = 50_000; + var concurrency = 50; + var asyncKeyedLocker = new AsyncKeyedLocker(o => + { + o.PoolSize = 50_000; + o.PoolInitialFill = 0; + }, Environment.ProcessorCount, 50_000); + var concurrentQueue = new ConcurrentQueue<(bool entered, int key)>(); + + var tasks = Enumerable.Range(1, locks * concurrency) + .Select(async i => + { + var key = Convert.ToInt32(Math.Ceiling((double)i / concurrency)); + using (await asyncKeyedLocker.LockAsync(key)) + { + await Task.Delay(20); + concurrentQueue.Enqueue((true, key)); + await Task.Delay(80); concurrentQueue.Enqueue((false, key)); } }); @@ -172,8 +231,9 @@ public async Task BasicTestGenericsPoolingProcessorCount() var key = Convert.ToInt32(Math.Ceiling((double)i / concurrency)); using (await asyncKeyedLocker.LockAsync(key)) { + await Task.Delay(20); concurrentQueue.Enqueue((true, key)); - await Task.Delay(10); + await Task.Delay(80); concurrentQueue.Enqueue((false, key)); } }); @@ -223,8 +283,9 @@ public async Task BasicTestGenericsPooling10k() var key = Convert.ToInt32(Math.Ceiling((double)i / concurrency)); using (await asyncKeyedLocker.LockAsync(key)) { + await Task.Delay(20); concurrentQueue.Enqueue((true, key)); - await Task.Delay(10); + await Task.Delay(80); concurrentQueue.Enqueue((false, key)); } }); @@ -274,8 +335,9 @@ public async Task BasicTestGenericsString() var key = Convert.ToInt32(Math.Ceiling((double)i / 5)).ToString(); using (await asyncKeyedLocker.LockAsync(key)) { + await Task.Delay(20); concurrentQueue.Enqueue((true, key)); - await Task.Delay(100); + await Task.Delay(80); concurrentQueue.Enqueue((false, key)); } }); diff --git a/AsyncKeyedLock.Tests/TestsForAsyncKeyedLock.cs b/AsyncKeyedLock.Tests/TestsForAsyncKeyedLock.cs new file mode 100644 index 0000000..6859cc1 --- /dev/null +++ b/AsyncKeyedLock.Tests/TestsForAsyncKeyedLock.cs @@ -0,0 +1,225 @@ +using FluentAssertions; +using System.Collections.Concurrent; +using Xunit; + +namespace AsyncKeyedLock.Tests; + +/// +/// Adapted from https://github.com/amoerie/keyed-semaphores/blob/main/KeyedSemaphores.Tests/TestsForKeyedSemaphore.cs +/// +public class TestsForAsyncKeyedLock +{ + private static readonly AsyncKeyedLocker _asyncKeyedLocker = new AsyncKeyedLocker(); + + public class Async : TestsForAsyncKeyedLock + { + [Fact] + public async Task ShouldRunThreadsWithDistinctKeysInParallel() + { + // Arrange + var currentParallelism = 0; + var maxParallelism = 0; + var parallelismLock = new object(); + + // 100 threads, 100 keys + var threads = Enumerable.Range(0, 100) + .Select(i => Task.Run(async () => await OccupyTheLockALittleBit(i).ConfigureAwait(false))) + .ToList(); + + // Act + await Task.WhenAll(threads).ConfigureAwait(false); + + maxParallelism.Should().BeGreaterThan(10); + + async Task OccupyTheLockALittleBit(int key) + { + using (await _asyncKeyedLocker.LockAsync(key.ToString())) + { + var incrementedCurrentParallelism = Interlocked.Increment(ref currentParallelism); + + lock (parallelismLock) + { + maxParallelism = Math.Max(incrementedCurrentParallelism, maxParallelism); + } + + const int delay = 250; + + await Task.Delay(TimeSpan.FromMilliseconds(delay)).ConfigureAwait(false); + + Interlocked.Decrement(ref currentParallelism); + } + } + } + + [Fact] + public async Task ShouldRunThreadsWithSameKeysLinearly() + { + // Arrange + var runningTasksIndex = new ConcurrentDictionary(); + var parallelismLock = new object(); + var currentParallelism = 0; + var maxParallelism = 0; + + // 100 threads, 10 keys + var threads = Enumerable.Range(0, 100) + .Select(i => Task.Run(async () => await OccupyTheLockALittleBit(i % 10).ConfigureAwait(false))) + .ToList(); + + // Act + Assert + await Task.WhenAll(threads).ConfigureAwait(false); + + maxParallelism.Should().BeLessOrEqualTo(10); + + async Task OccupyTheLockALittleBit(int key) + { + using (await _asyncKeyedLocker.LockAsync(key.ToString())) + { + var incrementedCurrentParallelism = Interlocked.Increment(ref currentParallelism); + + + lock (parallelismLock) + { + maxParallelism = Math.Max(incrementedCurrentParallelism, maxParallelism); + } + + var currentTaskId = Task.CurrentId ?? -1; + if (runningTasksIndex.TryGetValue(key, out var otherThread)) + throw new Exception($"Thread #{currentTaskId} acquired a lock using key ${key} " + + $"but another thread #{otherThread} is also still running using this key!"); + + runningTasksIndex[key] = currentTaskId; + + const int delay = 10; + + await Task.Delay(TimeSpan.FromMilliseconds(delay)).ConfigureAwait(false); + + if (!runningTasksIndex.TryRemove(key, out var value)) + { + var ex = new Exception($"Thread #{currentTaskId} has finished " + + "but when trying to cleanup the running threads index, the value is already gone"); + + throw ex; + } + + if (value != currentTaskId) + { + var ex = new Exception($"Thread #{currentTaskId} has finished and has removed itself from the running threads index," + + $" but that index contained an incorrect value: #{value}!"); + + throw ex; + } + + Interlocked.Decrement(ref currentParallelism); + } + } + } + } + + public class Sync : TestsForAsyncKeyedLock + { + [Fact] + public void ShouldRunThreadsWithDistinctKeysInParallel() + { + // Arrange + var currentParallelism = 0; + var maxParallelism = 0; + var parallelismLock = new object(); + + // 100 threads, 100 keys + var threads = Enumerable.Range(0, 100) + .Select(i => new Thread(() => OccupyTheLockALittleBit(i))) + .ToList(); + + // Act + foreach (var thread in threads) thread.Start(); + + foreach (var thread in threads) thread.Join(); + + maxParallelism.Should().BeGreaterThan(10); + + void OccupyTheLockALittleBit(int key) + { + using (_asyncKeyedLocker.Lock(key.ToString())) + { + var incrementedCurrentParallelism = Interlocked.Increment(ref currentParallelism); + + lock (parallelismLock) + { + maxParallelism = Math.Max(incrementedCurrentParallelism, maxParallelism); + } + + const int delay = 250; + + Thread.Sleep(TimeSpan.FromMilliseconds(delay)); + + Interlocked.Decrement(ref currentParallelism); + } + } + } + + [Fact] + public void ShouldRunThreadsWithSameKeysLinearly() + { + // Arrange + var runningThreadsIndex = new ConcurrentDictionary(); + var parallelismLock = new object(); + var currentParallelism = 0; + var maxParallelism = 0; + + // 100 threads, 10 keys + var threads = Enumerable.Range(0, 100) + .Select(i => new Thread(() => OccupyTheLockALittleBit(i % 10))) + .ToList(); + + // Act + foreach (var thread in threads) thread.Start(); + + foreach (var thread in threads) thread.Join(); + + // Assert + maxParallelism.Should().BeLessOrEqualTo(10); + + void OccupyTheLockALittleBit(int key) + { + using (_asyncKeyedLocker.Lock(key.ToString())) + { + var incrementedCurrentParallelism = Interlocked.Increment(ref currentParallelism); + + lock (parallelismLock) + { + maxParallelism = Math.Max(incrementedCurrentParallelism, maxParallelism); + } + + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + if (runningThreadsIndex.TryGetValue(key, out var otherThread)) + throw new Exception($"Thread #{currentThreadId} acquired a lock using key ${key} " + + $"but another thread #{otherThread} is also still running using this key!"); + + runningThreadsIndex[key] = currentThreadId; + + const int delay = 10; + + Thread.Sleep(TimeSpan.FromMilliseconds(delay)); + + if (!runningThreadsIndex.TryRemove(key, out var value)) + { + var ex = new Exception($"Thread #{currentThreadId} has finished " + + "but when trying to cleanup the running threads index, the value is already gone"); + + throw ex; + } + + if (value != currentThreadId) + { + var ex = new Exception($"Thread #{currentThreadId} has finished and has removed itself from the running threads index," + + $" but that index contained an incorrect value: #{value}!"); + + throw ex; + } + + Interlocked.Decrement(ref currentParallelism); + } + } + } + } +} diff --git a/AsyncKeyedLock.Tests/TestsForAsyncKeyedLockDictionary.cs b/AsyncKeyedLock.Tests/TestsForAsyncKeyedLockDictionary.cs new file mode 100644 index 0000000..2792b53 --- /dev/null +++ b/AsyncKeyedLock.Tests/TestsForAsyncKeyedLockDictionary.cs @@ -0,0 +1,226 @@ +using FluentAssertions; +using System.Collections.Concurrent; +using Xunit; + +namespace AsyncKeyedLock.Tests; + +/// +/// Adapted from https://github.com/amoerie/keyed-semaphores/blob/main/KeyedSemaphores.Tests/TestsForKeyedSemaphoresCollection.cs +/// +public class TestsForAsyncKeyedLockDictionary +{ + [Fact] + public async Task ShouldRunThreadsWithDistinctKeysInParallel() + { + // Arrange + var currentParallelism = 0; + var maxParallelism = 0; + var parallelismLock = new object(); + var asyncKeyedLocks = new AsyncKeyedLocker(); + var index = asyncKeyedLocks.Index; + + // 100 threads, 100 keys + var threads = Enumerable.Range(0, 100) + .Select(i => Task.Run(async () => await OccupyTheLockALittleBit(i).ConfigureAwait(false))) + .ToList(); + + // Act + await Task.WhenAll(threads).ConfigureAwait(false); + + maxParallelism.Should().BeGreaterThan(10); + index.Should().BeEmpty(); + + async Task OccupyTheLockALittleBit(int key) + { + using (await asyncKeyedLocks.LockAsync(key.ToString())) + { + var incrementedCurrentParallelism = Interlocked.Increment(ref currentParallelism); + + lock (parallelismLock) + { + maxParallelism = Math.Max(incrementedCurrentParallelism, maxParallelism); + } + + const int delay = 250; + + + await Task.Delay(TimeSpan.FromMilliseconds(delay)).ConfigureAwait(false); + + Interlocked.Decrement(ref currentParallelism); + } + } + } + + [Fact] + public async Task ShouldRunThreadsWithSameKeysLinearly() + { + // Arrange + var runningTasksIndex = new ConcurrentDictionary(); + var parallelismLock = new object(); + var currentParallelism = 0; + var maxParallelism = 0; + var asyncKeyedLocks = new AsyncKeyedLocker(); + var index = asyncKeyedLocks.Index; + + // 100 threads, 10 keys + var threads = Enumerable.Range(0, 100) + .Select(i => Task.Run(async () => await OccupyTheLockALittleBit(i % 10).ConfigureAwait(false))) + .ToList(); + + // Act + Assert + await Task.WhenAll(threads).ConfigureAwait(false); + + maxParallelism.Should().BeLessOrEqualTo(10); + index.Should().BeEmpty(); + + async Task OccupyTheLockALittleBit(int key) + { + using (await asyncKeyedLocks.LockAsync(key)) + { + var incrementedCurrentParallelism = Interlocked.Increment(ref currentParallelism); + + lock (parallelismLock) + { + maxParallelism = Math.Max(incrementedCurrentParallelism, maxParallelism); + } + + var currentTaskId = Task.CurrentId ?? -1; + if (runningTasksIndex.TryGetValue(key, out var otherThread)) + throw new Exception($"Thread #{currentTaskId} acquired a lock using key ${key} " + + $"but another thread #{otherThread} is also still running using this key!"); + + runningTasksIndex[key] = currentTaskId; + + const int delay = 10; + + await Task.Delay(TimeSpan.FromMilliseconds(delay)).ConfigureAwait(false); + + if (!runningTasksIndex.TryRemove(key, out var value)) + { + var ex = new Exception($"Thread #{currentTaskId} has finished " + + "but when trying to cleanup the running threads index, the value is already gone"); + + throw ex; + } + + if (value != currentTaskId) + { + var ex = new Exception($"Thread #{currentTaskId} has finished and has removed itself from the running threads index," + + $" but that index contained an incorrect value: #{value}!"); + + throw ex; + } + + Interlocked.Decrement(ref currentParallelism); + } + } + } + + [Fact] + public async Task ShouldNeverCreateTwoSemaphoresForTheSameKey() + { + // Arrange + var runningTasksIndex = new ConcurrentDictionary(); + var parallelismLock = new object(); + var currentParallelism = 0; + var maxParallelism = 0; + var random = new Random(); + var asyncKeyedLocks = new AsyncKeyedLocker(); + var index = asyncKeyedLocks.Index; + + // Many threads, 1 key + var threads = Enumerable.Range(0, 100) + .Select(_ => Task.Run(async () => await OccupyTheLockALittleBit(1).ConfigureAwait(false))) + .ToList(); + + // Act + Assert + await Task.WhenAll(threads).ConfigureAwait(false); + + maxParallelism.Should().Be(1); + index.Should().BeEmpty(); + + + async Task OccupyTheLockALittleBit(int key) + { + var currentTaskId = Task.CurrentId ?? -1; + var delay = random.Next(500); + + await Task.Delay(delay).ConfigureAwait(false); + + using (await asyncKeyedLocks.LockAsync(key)) + { + var incrementedCurrentParallelism = Interlocked.Increment(ref currentParallelism); + + lock (parallelismLock) + { + maxParallelism = Math.Max(incrementedCurrentParallelism, maxParallelism); + } + + if (runningTasksIndex.TryGetValue(key, out var otherThread)) + throw new Exception($"Task [{currentTaskId,3}] has a lock for key ${key} " + + $"but another task [{otherThread,3}] also has an active lock for this key!"); + + runningTasksIndex[key] = currentTaskId; + + if (!runningTasksIndex.TryRemove(key, out var value)) + { + var ex = new Exception($"Task [{currentTaskId,3}] has finished " + + "but when trying to cleanup the running tasks index, the value is already gone"); + + throw ex; + } + + if (value != currentTaskId) + { + var ex = new Exception($"Task [{currentTaskId,3}] has finished and has removed itself from the running tasks index," + + $" but that index contained a task ID of another task: [{value}]!"); + + throw ex; + } + + Interlocked.Decrement(ref currentParallelism); + } + } + } + + [Fact] + public async Task ShouldRunThreadsWithDistinctStringKeysInParallel() + { + // Arrange + var currentParallelism = 0; + var maxParallelism = 0; + var parallelismLock = new object(); + var asyncKeyedLocks = new AsyncKeyedLocker(); + var index = asyncKeyedLocks.Index; + + // 100 threads, 100 keys + var threads = Enumerable.Range(0, 100) + .Select(i => Task.Run(async () => await OccupyTheLockALittleBit(i).ConfigureAwait(false))) + .ToList(); + + // Act + await Task.WhenAll(threads).ConfigureAwait(false); + + maxParallelism.Should().BeGreaterThan(10); + index.Should().BeEmpty(); + + async Task OccupyTheLockALittleBit(int key) + { + using (await asyncKeyedLocks.LockAsync(key.ToString())) + { + var incrementedCurrentParallelism = Interlocked.Increment(ref currentParallelism); + + lock (parallelismLock) + { + maxParallelism = Math.Max(incrementedCurrentParallelism, maxParallelism); + } + + const int delay = 250; + + await Task.Delay(TimeSpan.FromMilliseconds(delay)).ConfigureAwait(false); + + Interlocked.Decrement(ref currentParallelism); + } + } + } +} \ No newline at end of file diff --git a/AsyncKeyedLock.Tests/TestsForCancellationAndTimeout.cs b/AsyncKeyedLock.Tests/TestsForCancellationAndTimeout.cs new file mode 100644 index 0000000..b1e5605 --- /dev/null +++ b/AsyncKeyedLock.Tests/TestsForCancellationAndTimeout.cs @@ -0,0 +1,308 @@ +using FluentAssertions; +using Xunit; + +namespace AsyncKeyedLock.Tests; + +/// +/// Adapted from https://github.com/amoerie/keyed-semaphores/blob/main/KeyedSemaphores.Tests/TestsForCancellationAndTimeout.cs +/// +public class TestForCancellationTokenAndTimeout +{ + [Fact] + public void Lock_WhenCancelled_ShouldReleaseKeyedSemaphoreAndThrowOperationCanceledException() + { + // Arrange + var collection = new AsyncKeyedLocker(); + var cancelledCancellationToken = new CancellationToken(true); + + // Act + var action = () => + { + using var _ = collection.Lock("test", cancelledCancellationToken); + }; + action.Should().Throw(); + + // Assert + collection.Index.Should().NotContainKey("test"); + } + + [Fact] + public void Lock_WhenNotCancelled_ShouldReturnDisposable() + { + // Arrange + var collection = new AsyncKeyedLocker(); + var cancellationToken = default(CancellationToken); + + // Act + var releaser = collection.Lock("test", cancellationToken); + + // Assert + collection.Index["test"].ReferenceCount.Should().Be(1); + releaser.Dispose(); + collection.Index.Should().NotContainKey("test"); + } + + [Fact] + public void TryLock_WhenCancelled_ShouldReleaseKeyedSemaphoreAndThrowOperationCanceledExceptionAndNotInvokeCallback() + { + // Arrange + var isLockAcquired = false; + var isCallbackInvoked = false; + void Callback() + { + isCallbackInvoked = true; + } + var collection = new AsyncKeyedLocker(); + var cancelledCancellationToken = new CancellationToken(true); + + // Act + var action = () => + { + isLockAcquired = collection.TryLock("test", Callback, TimeSpan.FromMinutes(1), cancelledCancellationToken); + }; + action.Should().Throw(); + + // Assert + collection.Index.Should().NotContainKey("test"); + isLockAcquired.Should().BeFalse(); + isCallbackInvoked.Should().BeFalse(); + } + + [Fact] + public void TryLock_WhenNotCancelled_ShouldInvokeCallbackAndReturnDisposable() + { + // Arrange + bool isCallbackInvoked = false; + void Callback() + { + isCallbackInvoked = true; + } + var collection = new AsyncKeyedLocker(); + var cancellationToken = default(CancellationToken); + + // Act + var isLockAcquired = collection.TryLock("test", Callback, TimeSpan.FromMinutes(1), cancellationToken); + + // Assert + collection.Index.Should().NotContainKey("test"); + isLockAcquired.Should().BeTrue(); + isCallbackInvoked.Should().BeTrue(); + } + + [Fact] + public async Task LockAsync_WhenCancelled_ShouldReleaseKeyedSemaphoreAndThrowOperationCanceledException() + { + // Arrange + var collection = new AsyncKeyedLocker(); + var cancelledCancellationToken = new CancellationToken(true); + + // Act + var action = async () => + { + using var _ = await collection.LockAsync("test", cancelledCancellationToken); + }; + await action.Should().ThrowAsync(); + + // Assert + collection.Index.Should().NotContainKey("test"); + } + + [Fact] + public async Task LockAsync_WhenNotCancelled_ShouldReturnDisposable() + { + // Arrange + var collection = new AsyncKeyedLocker(); + var cancellationToken = default(CancellationToken); + + // Act + var releaser = await collection.LockAsync("test", cancellationToken); + + // Assert + collection.Index["test"].ReferenceCount.Should().Be(1); + releaser.Dispose(); + collection.Index.Should().NotContainKey("test"); + } + + [Fact] + public async Task TryLockAsync_WithSynchronousCallback_WhenCancelled_ShouldReleaseKeyedSemaphoreAndThrowOperationCanceledExceptionAndNotInvokeCallback() + { + // Arrange + bool isLockAcquired = false; + bool isCallbackInvoked = false; + void Callback() + { + isCallbackInvoked = true; + } + var collection = new AsyncKeyedLocker(); + var cancelledCancellationToken = new CancellationToken(true); + + // Act + var action = async () => + { + isLockAcquired = await collection.TryLockAsync("test", Callback, TimeSpan.FromMinutes(1), cancelledCancellationToken); + }; + await action.Should().ThrowAsync(); + + // Assert + collection.Index.Should().NotContainKey("test"); + isLockAcquired.Should().BeFalse(); + isCallbackInvoked.Should().BeFalse(); + } + + [Fact] + public async Task TryLockAsync_WithSynchronousCallback_WhenNotCancelled_ShouldInvokeCallbackAndReturnTrue() + { + // Arrange + var isCallbackInvoked = false; + void Callback() + { + isCallbackInvoked = true; + } + var collection = new AsyncKeyedLocker(); + var cancellationToken = default(CancellationToken); + + // Act + var isLockAcquired = await collection.TryLockAsync("test", Callback, TimeSpan.FromMinutes(1), cancellationToken); + + // Assert + collection.Index.Should().NotContainKey("test"); + isLockAcquired.Should().BeTrue(); + isCallbackInvoked.Should().BeTrue(); + } + + [Fact] + public async Task TryLockAsync_WithAsynchronousCallback_WhenCancelled_ShouldReleaseKeyedSemaphoreAndThrowOperationCanceledExceptionAndNotInvokeCallback() + { + // Arrange + bool isLockAcquired = false; + bool isCallbackInvoked = false; + + async Task Callback() + { + await Task.Delay(1); + isCallbackInvoked = true; + } + var collection = new AsyncKeyedLocker(); + var cancelledCancellationToken = new CancellationToken(true); + + // Act + var action = async () => + { + isLockAcquired = await collection.TryLockAsync("test", Callback, TimeSpan.FromMinutes(1), cancelledCancellationToken); + }; + await action.Should().ThrowAsync(); + + // Assert + collection.Index.Should().NotContainKey("test"); + isLockAcquired.Should().BeFalse(); + isCallbackInvoked.Should().BeFalse(); + } + + [Fact] + public async Task TryLockAsync_WithAsynchronousCallback_WhenNotCancelled_ShouldInvokeCallbackAndReturnTrue() + { + // Arrange + var isCallbackInvoked = false; + async Task Callback() + { + await Task.Delay(1); + isCallbackInvoked = true; + } + var collection = new AsyncKeyedLocker(); + var cancellationToken = default(CancellationToken); + + // Act + var isLockAcquired = await collection.TryLockAsync("test", Callback, TimeSpan.FromMinutes(1), cancellationToken); + + // Assert + collection.Index.Should().NotContainKey("test"); + isLockAcquired.Should().BeTrue(); + isCallbackInvoked.Should().BeTrue(); + } + + [Fact] + public void TryLock_WhenTimedOut_ShouldNotInvokeCallbackAndReturnFalse() + { + // Arrange + var collection = new AsyncKeyedLocker(); + var key = "test"; + using var _ = collection.Lock(key); + var isCallbackInvoked = false; + void Callback() + { + isCallbackInvoked = true; + } + + // Act + var isLockAcquired = collection.TryLock(key, Callback, TimeSpan.FromSeconds(1)); + + // Assert + isLockAcquired.Should().BeFalse(); + isCallbackInvoked.Should().BeFalse(); + collection.Index[key].ReferenceCount.Should().Be(1); + } + + [Fact] + public void TryLock_WhenNotTimedOut_ShouldInvokeCallbackAndReturnTrue() + { + // Arrange + var collection = new AsyncKeyedLocker(); + var key = "test"; + var isCallbackInvoked = false; + void Callback() + { + isCallbackInvoked = true; + } + + // Act + var isLockAcquired = collection.TryLock(key, Callback, TimeSpan.FromSeconds(1)); + + // Assert + isLockAcquired.Should().BeTrue(); + isCallbackInvoked.Should().BeTrue(); + collection.Index.Should().NotContainKey(key); + } + + [Fact] + public async Task TryLockAsync_WhenTimedOut_ShouldNotInvokeCallbackAndReturnFalse() + { + // Arrange + var collection = new AsyncKeyedLocker(); + var key = "test"; + using var _ = await collection.LockAsync(key); + var isCallbackInvoked = false; + void Callback() + { + isCallbackInvoked = true; + } + + // Act + var isLockAcquired = await collection.TryLockAsync(key, Callback, TimeSpan.FromSeconds(1)); + + // Assert + isLockAcquired.Should().BeFalse(); + isCallbackInvoked.Should().BeFalse(); + collection.Index[key].ReferenceCount.Should().Be(1); + } + + [Fact] + public async Task TryLockAsync_WhenNotTimedOut_ShouldNotInvokeCallbackAndReturnFalse() + { + // Arrange + var collection = new AsyncKeyedLocker(); + var key = "test"; + var isCallbackInvoked = false; + void Callback() + { + isCallbackInvoked = true; + } + + // Act + var isLockAcquired = await collection.TryLockAsync(key, Callback, TimeSpan.FromSeconds(1)); + + // Assert + isLockAcquired.Should().BeTrue(); + isCallbackInvoked.Should().BeTrue(); + collection.Index.Should().NotContainKey(key); + } +} \ No newline at end of file diff --git a/AsyncKeyedLock/AsyncKeyedLock.csproj b/AsyncKeyedLock/AsyncKeyedLock.csproj index 64f0d1f..4e7c372 100644 --- a/AsyncKeyedLock/AsyncKeyedLock.csproj +++ b/AsyncKeyedLock/AsyncKeyedLock.csproj @@ -2,22 +2,22 @@ netstandard2.0;net5.0 - 10.0 + 8.0 Mark Cilia Vincenti https://github.com/MarkCiliaVincenti/AsyncKeyedLock.git https://github.com/MarkCiliaVincenti/AsyncKeyedLock MIT MIT - 5.1.3-beta + 6.0.0 logo.png - Updated licensing, small optimisation, fixed README shields issue. + 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. © 2022 Mark Cilia Vincenti async,lock,key,keyed,semaphore,dictionary,pooling,duplicate,synchronization git false - 5.1.3.1 - 5.1.3.1 + 6.0.0.0 + 6.0.0.0 README.md true true diff --git a/AsyncKeyedLock/AsyncKeyedLockDictionary.cs b/AsyncKeyedLock/AsyncKeyedLockDictionary.cs index dca2207..b7b69c9 100644 --- a/AsyncKeyedLock/AsyncKeyedLockDictionary.cs +++ b/AsyncKeyedLock/AsyncKeyedLockDictionary.cs @@ -142,5 +142,24 @@ public void Release(AsyncKeyedLockReleaser releaser) Monitor.Exit(releaser); releaser.SemaphoreSlim.Release(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReleaseWithoutSemaphoreRelease(AsyncKeyedLockReleaser releaser) + { + Monitor.Enter(releaser); + + if (--releaser.ReferenceCount == 0) + { + TryRemove(releaser.Key, out _); + Monitor.Exit(releaser); + if (_poolingEnabled) + { + _pool.PutObject(releaser); + } + return; + } + + Monitor.Exit(releaser); + } } } \ No newline at end of file diff --git a/AsyncKeyedLock/AsyncKeyedLocker.cs b/AsyncKeyedLock/AsyncKeyedLocker.cs index 3a320b1..3fd2b6f 100644 --- a/AsyncKeyedLock/AsyncKeyedLocker.cs +++ b/AsyncKeyedLock/AsyncKeyedLocker.cs @@ -156,6 +156,10 @@ public AsyncKeyedLocker(Action options, int concurrencyLe public class AsyncKeyedLocker where TKey : notnull { private readonly AsyncKeyedLockDictionary _dictionary; + /// + /// Read-only index of objects held by . + /// + public IReadOnlyDictionary> Index => _dictionary; /// /// The maximum number of requests for the semaphore that can be granted concurrently. Defaults to 1. @@ -187,7 +191,7 @@ public AsyncKeyedLocker(AsyncKeyedLockOptions options) /// Parameter is out of range. public AsyncKeyedLocker(Action options) { - AsyncKeyedLockOptions optionsParam = new(); + var optionsParam = new AsyncKeyedLockOptions(); options(optionsParam); _dictionary = new AsyncKeyedLockDictionary(optionsParam); @@ -224,7 +228,7 @@ public AsyncKeyedLocker(AsyncKeyedLockOptions options, IEqualityComparer c /// is null. public AsyncKeyedLocker(Action options, IEqualityComparer comparer) { - AsyncKeyedLockOptions optionsParam = new(); + var optionsParam = new AsyncKeyedLockOptions(); options(optionsParam); _dictionary = new AsyncKeyedLockDictionary(optionsParam, comparer); @@ -262,7 +266,7 @@ public AsyncKeyedLocker(AsyncKeyedLockOptions options, int concurrencyLevel, int /// Parameter is out of range. public AsyncKeyedLocker(Action options, int concurrencyLevel, int capacity) { - AsyncKeyedLockOptions optionsParam = new(); + var optionsParam = new AsyncKeyedLockOptions(); options(optionsParam); _dictionary = new AsyncKeyedLockDictionary(optionsParam, concurrencyLevel, capacity); @@ -306,7 +310,7 @@ public AsyncKeyedLocker(AsyncKeyedLockOptions options, int concurrencyLevel, int /// is null. public AsyncKeyedLocker(Action options, int concurrencyLevel, int capacity, IEqualityComparer comparer) { - AsyncKeyedLockOptions optionsParam = new(); + var optionsParam = new AsyncKeyedLockOptions(); options(optionsParam); _dictionary = new AsyncKeyedLockDictionary(optionsParam, concurrencyLevel, capacity, comparer); @@ -321,6 +325,8 @@ public AsyncKeyedLocker(Action options, int concurrencyLe public AsyncKeyedLockReleaser GetOrAdd(TKey key) => _dictionary.GetOrAdd(key); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Release(AsyncKeyedLockReleaser releaser) => _dictionary.Release(releaser); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReleaseWithoutSemaphoreRelease(AsyncKeyedLockReleaser releaser) => _dictionary.ReleaseWithoutSemaphoreRelease(releaser); #region Synchronous @@ -351,156 +357,140 @@ public IDisposable Lock(TKey key, CancellationToken cancellationToken) } catch (OperationCanceledException) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); throw; } return releaser; } + #endregion Synchronous + #region SynchronousTry /// - /// Synchronously lock based on a key, setting a limit for the number of milliseconds to wait. + /// Synchronously lock based on a key, setting a limit for the number of milliseconds to wait, and if not timed out, scynchronously execute an action and release. /// /// The key to lock on. + /// The synchronous action. /// 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) + /// False if timed out, true if it successfully entered. + public bool TryLock(TKey key, Action action, int millisecondsTimeout) { var releaser = GetOrAdd(key); - releaser.SemaphoreSlim.Wait(millisecondsTimeout); - return releaser; - } + if (!releaser.SemaphoreSlim.Wait(millisecondsTimeout)) + { + ReleaseWithoutSemaphoreRelease(releaser); + return false; + } - /// - /// 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. - /// False if timed out, true if it successfully entered. - /// A disposable value. - public IDisposable Lock(TKey key, int millisecondsTimeout, out bool success) - { - var releaser = GetOrAdd(key); - success = releaser.SemaphoreSlim.Wait(millisecondsTimeout); - return releaser; + try + { + action(); + } + finally + { + Release(releaser); + } + return true; } /// - /// Synchronously lock based on a key, setting a limit for the to wait. + /// Synchronously lock based on a key, setting a limit for the to wait, and if not timed out, scynchronously execute an action and release. /// /// The key to lock on. + /// The synchronous action. /// 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) + /// False if timed out, true if it successfully entered. + public bool TryLock(TKey key, Action action, TimeSpan timeout) { var releaser = GetOrAdd(key); - releaser.SemaphoreSlim.Wait(timeout); - return releaser; - } + if (!releaser.SemaphoreSlim.Wait(timeout)) + { + ReleaseWithoutSemaphoreRelease(releaser); + return false; + } - /// - /// 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. - /// False if timed out, true if it successfully entered. - /// A disposable value. - public IDisposable Lock(TKey key, TimeSpan timeout, out bool success) - { - var releaser = GetOrAdd(key); - success = releaser.SemaphoreSlim.Wait(timeout); - return releaser; + try + { + action(); + } + finally + { + Release(releaser); + } + return true; } /// - /// Synchronously lock based on a key, setting a limit for the number of milliseconds to wait, while observing a . + /// Synchronously lock based on a key, setting a limit for the number of milliseconds to wait, and if not timed out, scynchronously execute an action and release, while observing a . /// /// The key to lock on. + /// The synchronous action. /// 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) + /// False if timed out, true if it successfully entered. + public bool TryLock(TKey key, Action action, int millisecondsTimeout, CancellationToken cancellationToken) { var releaser = GetOrAdd(key); try { - releaser.SemaphoreSlim.Wait(millisecondsTimeout, cancellationToken); + if (!releaser.SemaphoreSlim.Wait(millisecondsTimeout, cancellationToken)) + { + ReleaseWithoutSemaphoreRelease(releaser); + return false; + } } catch (OperationCanceledException) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); throw; } - return 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. - /// False if timed out, true if it successfully entered. - /// A disposable value. - public IDisposable Lock(TKey key, int millisecondsTimeout, CancellationToken cancellationToken, out bool success) - { - var releaser = GetOrAdd(key); try { - success = releaser.SemaphoreSlim.Wait(millisecondsTimeout, cancellationToken); + action(); } - catch (OperationCanceledException) + finally { Release(releaser); - throw; } - return releaser; + return true; } /// - /// Synchronously lock based on a key, setting a limit for the to wait, while observing a . + /// Synchronously lock based on a key, setting a limit for the to wait, and if not timed out, scynchronously execute an action and release, while observing a . /// /// The key to lock on. + /// The synchronous action. /// 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) + /// False if timed out, true if it successfully entered. + public bool TryLock(TKey key, Action action, TimeSpan timeout, CancellationToken cancellationToken) { var releaser = GetOrAdd(key); try { - releaser.SemaphoreSlim.Wait(timeout, cancellationToken); + if (!releaser.SemaphoreSlim.Wait(timeout, cancellationToken)) + { + ReleaseWithoutSemaphoreRelease(releaser); + return false; + } } catch (OperationCanceledException) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); throw; } - return releaser; - } - /// - /// 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. - /// False if timed out, true if it successfully entered. - /// A disposable value. - public IDisposable Lock(TKey key, TimeSpan timeout, CancellationToken cancellationToken, out bool success) - { - var releaser = GetOrAdd(key); try { - success = releaser.SemaphoreSlim.Wait(timeout, cancellationToken); + action(); } - catch (OperationCanceledException) + finally { Release(releaser); - throw; } - return releaser; + return true; } - #endregion Synchronous + #endregion SynchronousTry #region AsynchronousTry /// @@ -515,7 +505,7 @@ public async ValueTask TryLockAsync(TKey key, Action action, int milliseco var releaser = GetOrAdd(key); if (!await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout).ConfigureAwait(false)) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); return false; } @@ -523,10 +513,6 @@ public async ValueTask TryLockAsync(TKey key, Action action, int milliseco { action(); } - catch - { - throw; - } finally { Release(releaser); @@ -535,7 +521,7 @@ public async ValueTask TryLockAsync(TKey key, Action action, int milliseco } /// - /// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait, and if not timed out, scynchronously execute an action and release. + /// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait, and if not timed out, ascynchronously execute a and release. /// /// The key to lock on. /// The asynchronous task. @@ -546,7 +532,7 @@ public async ValueTask TryLockAsync(TKey key, Func task, int millise var releaser = GetOrAdd(key); if (!await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout).ConfigureAwait(false)) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); return false; } @@ -554,10 +540,6 @@ public async ValueTask TryLockAsync(TKey key, Func task, int millise { await task().ConfigureAwait(false); } - catch - { - throw; - } finally { Release(releaser); @@ -577,7 +559,7 @@ public async ValueTask TryLockAsync(TKey key, Action action, TimeSpan time var releaser = GetOrAdd(key); if (!await releaser.SemaphoreSlim.WaitAsync(timeout).ConfigureAwait(false)) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); return false; } @@ -585,10 +567,6 @@ public async ValueTask TryLockAsync(TKey key, Action action, TimeSpan time { action(); } - catch - { - throw; - } finally { Release(releaser); @@ -597,7 +575,7 @@ public async ValueTask TryLockAsync(TKey key, Action action, TimeSpan time } /// - /// Asynchronously lock based on a key, setting a limit for the to wait, and if not timed out, scynchronously execute an action and release. + /// Asynchronously lock based on a key, setting a limit for the to wait, and if not timed out, ascynchronously execute a and release. /// /// The key to lock on. /// The asynchronous task. @@ -608,7 +586,7 @@ public async ValueTask TryLockAsync(TKey key, Func task, TimeSpan ti var releaser = GetOrAdd(key); if (!await releaser.SemaphoreSlim.WaitAsync(timeout).ConfigureAwait(false)) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); return false; } @@ -616,10 +594,6 @@ public async ValueTask TryLockAsync(TKey key, Func task, TimeSpan ti { await task().ConfigureAwait(false); } - catch - { - throw; - } finally { Release(releaser); @@ -642,13 +616,13 @@ public async ValueTask TryLockAsync(TKey key, Action action, int milliseco { if (!await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout, cancellationToken).ConfigureAwait(false)) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); return false; } } catch (OperationCanceledException) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); throw; } @@ -656,10 +630,6 @@ public async ValueTask TryLockAsync(TKey key, Action action, int milliseco { action(); } - catch - { - throw; - } finally { Release(releaser); @@ -668,7 +638,7 @@ public async ValueTask TryLockAsync(TKey key, Action action, int milliseco } /// - /// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait, and if not timed out, scynchronously execute an action and release, while observing a . + /// Asynchronously lock based on a key, setting a limit for the number of milliseconds to wait, and if not timed out, ascynchronously execute a and release, while observing a . /// /// The key to lock on. /// The asynchronous task. @@ -682,13 +652,13 @@ public async ValueTask TryLockAsync(TKey key, Func task, int millise { if (!await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout, cancellationToken).ConfigureAwait(false)) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); return false; } } catch (OperationCanceledException) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); throw; } @@ -696,10 +666,6 @@ public async ValueTask TryLockAsync(TKey key, Func task, int millise { await task().ConfigureAwait(false); } - catch - { - throw; - } finally { Release(releaser); @@ -722,13 +688,13 @@ public async ValueTask TryLockAsync(TKey key, Action action, TimeSpan time { if (!await releaser.SemaphoreSlim.WaitAsync(timeout, cancellationToken).ConfigureAwait(false)) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); return false; } } catch (OperationCanceledException) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); throw; } @@ -736,10 +702,6 @@ public async ValueTask TryLockAsync(TKey key, Action action, TimeSpan time { action(); } - catch - { - throw; - } finally { Release(releaser); @@ -748,7 +710,7 @@ public async ValueTask TryLockAsync(TKey key, Action action, TimeSpan time } /// - /// Asynchronously lock based on a key, setting a limit for the to wait, and if not timed out, scynchronously execute an action and release, while observing a . + /// Asynchronously lock based on a key, setting a limit for the to wait, and if not timed out, ascynchronously execute a and release, while observing a . /// /// The key to lock on. /// The asynchronous task. @@ -762,13 +724,13 @@ public async ValueTask TryLockAsync(TKey key, Func task, TimeSpan ti { if (!await releaser.SemaphoreSlim.WaitAsync(timeout, cancellationToken).ConfigureAwait(false)) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); return false; } } catch (OperationCanceledException) { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); throw; } @@ -776,10 +738,6 @@ public async ValueTask TryLockAsync(TKey key, Func task, TimeSpan ti { await task().ConfigureAwait(false); } - catch - { - throw; - } finally { Release(releaser); @@ -816,77 +774,7 @@ public async ValueTask LockAsync(TKey key, CancellationToken cancel } catch (OperationCanceledException) { - Release(releaser); - throw; - } - 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); - await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout).ConfigureAwait(false); - return 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); - await releaser.SemaphoreSlim.WaitAsync(timeout).ConfigureAwait(false); - return 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 - { - await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - Release(releaser); - throw; - } - return releaser; - } - - /// - /// 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 - { - await releaser.SemaphoreSlim.WaitAsync(timeout, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - Release(releaser); + ReleaseWithoutSemaphoreRelease(releaser); throw; } return releaser; diff --git a/README.md b/README.md index f5f0ffd..790852a 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,11 @@ var asyncKeyedLocker = new AsyncKeyedLocker(); or if you would like to set the maximum number of requests for the semaphore that can be granted concurrently (set to 1 by default): ```csharp -var asyncKeyedLocker = new AsyncKeyedLocker(new AsyncKeyedLockOptions(maxCount: 2)); +// using AsyncKeyedLockOptions +var asyncKeyedLocker1 = new AsyncKeyedLocker(new AsyncKeyedLockOptions(maxCount: 2)); + +// using Action +var asyncKeyedLocker2 = new AsyncKeyedLocker(o => o.MaxCount = 2); ``` There are also AsyncKeyedLocker() constructors which accept the parameters of ConcurrentDictionary, namely the concurrency level, the capacity and the IEqualityComparer to use. @@ -51,26 +55,45 @@ It is recommended to run benchmarks and tests if you intend on using pooling to Setting the pool size can be done via the `AsyncKeyedLockOptions` in one of the overloaded constructors, such as this: ```csharp -var asyncKeyedLocker = new AsyncKeyedLocker(new AsyncKeyedLockOptions(poolSize: 100)); +// using AsyncKeyedLockOptions +var asyncKeyedLocker1 = new AsyncKeyedLocker(new AsyncKeyedLockOptions(poolSize: 100)); + +// using Action +var asyncKeyedLocker2 = new AsyncKeyedLocker(o => o.PoolSize = 100); ``` You can also set the initial pool fill (by default this is set to the pool size): ```csharp +// using AsyncKeyedLockOptions var asyncKeyedLocker = new AsyncKeyedLocker(new AsyncKeyedLockOptions(poolSize: 100, poolInitialFill: 50)); + +// using Action +var asyncKeyedLocker = new AsyncKeyedLocker(o => +{ + o.PoolSize = 100; + o.PoolInitialFill = 50; +}); ``` ### Locking ```csharp +// without cancellation token using (var lockObj = await asyncKeyedLocker.LockAsync(myObject)) { ... } + +// with cancellation token +using (var lockObj = await asyncKeyedLocker.LockAsync(myObject, cancellationToken)) +{ + ... +} ``` -There are other overloaded methods for `LockAsync` which allow you to use `CancellationToken`, milliseconds timeout, `System.TimeSpan` or a combination of these. In the case of 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. +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. -There are also synchronous `Lock` methods available, including out parameters for checking whether or not the timeout was reached. +There are also synchronous `Lock` and `TryLock` methods available. If you would like to see how many concurrent requests there are for a semaphore for a given key: ```csharp