diff --git a/README.md b/README.md index 28f0286..13c16e8 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,10 @@ A simple C# repository containing a few basic useful Thread-Safe Objects. - DataObject - Extentions - IBindableObjectExtention + - IMethodThreadLocksExtentions - INotifyCollectionChangedExtension - INotifyPropertyChangedExtension + - ISingleThreadLockExtentions - ObjectExtention - HelperClasses - ThreadHelper @@ -64,8 +66,8 @@ A simple C# repository containing a few basic useful Thread-Safe Objects. - IHashSetThreadSafe - ILinkedListThreadSafe - IListThreadSafe - - IMethodThreadLock - - IMultipleThreadLockManager\ + - IMethodThreadLocks + - IMultipleThreadLocks\ - IObservableCollectionThreadSafe - IObservableCollectionThreadSafe\ - IObservableDataCollection @@ -87,8 +89,8 @@ A simple C# repository containing a few basic useful Thread-Safe Objects. - Locks - GatedThreadLock - GateKeeper - - MethodThreadLock - - MultipleThreadLockManager + - MethodThreadLocks + - MultipleThreadLocks - SingleThreadLock ---- @@ -454,6 +456,17 @@ This can be overwritten durring creation or by setting Property `WaitOnNotifyCol Observable Objects Property `WaitOnNotifyPropertyChanged` has been renamed to Property `WaitOnNotifying`. Observable Collections Property `WaitOnNotifyCollectionChanged` has been removed and now uses Property `WaitOnNotifying`. + + +## Breaking changes from v3.1.1 to v3.1.2! + +Lock `MethodThreadLock` has been renamed to `MethodThreadLocks`. + +Interface `IMethodThreadLock` has been renamed to `IMethodThreadLocks`. + +Lock `MultipleThreadLockManager` has been renamed to `MultipleThreadLocks`. + +Interface `IMultipleThreadLockManager` has been renamed to `IMultipleThreadLocks`. ---- diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MethodThreadLockTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MethodThreadLocksTests.cs similarity index 64% rename from src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MethodThreadLockTests.cs rename to src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MethodThreadLocksTests.cs index b1aed6c..07cf2db 100644 --- a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MethodThreadLockTests.cs +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MethodThreadLocksTests.cs @@ -6,44 +6,44 @@ namespace ThunderDesign.Net_PCL.Threading.UnitTests.Locks { - public class MethodThreadLockTests + public class MethodThreadLocksTests { - private async Task MethodA(MethodThreadLock methodLock, int delayMs) + private async Task MethodA(MethodThreadLocks methodLocks, int delayMs) { - await methodLock.LockAsync(); + await methodLocks.LockAsync(); try { await Task.Delay(delayMs); } finally { - methodLock.Unlock(); + methodLocks.Unlock(); } } - private async Task MethodB(MethodThreadLock methodLock, int delayMs) + private async Task MethodB(MethodThreadLocks methodLocks, int delayMs) { - await methodLock.LockAsync(); + await methodLocks.LockAsync(); try { await Task.Delay(delayMs); } finally { - methodLock.Unlock(); + methodLocks.Unlock(); } } [Fact] public async Task LockAsync_ShouldUseCallerMemberName_ToScopeLock() { - using var methodLock = new MethodThreadLock(); + using var methodLocks = new MethodThreadLocks(); - var firstCall = MethodA(methodLock, 200); + var firstCall = MethodA(methodLocks, 200); await Task.Delay(20); // allow first call to acquire lock - var secondCall = MethodA(methodLock, 20); + var secondCall = MethodA(methodLocks, 20); var completedEarly = await Task.WhenAny(secondCall, Task.Delay(50)) == secondCall; Assert.False(completedEarly); @@ -54,10 +54,10 @@ public async Task LockAsync_ShouldUseCallerMemberName_ToScopeLock() [Fact] public async Task LockAsync_ShouldNotBlock_DifferentMethods() { - using var methodLock = new MethodThreadLock(); + using var methodLocks = new MethodThreadLocks(); - var callA = MethodA(methodLock, 200); - var callB = MethodB(methodLock, 200); + var callA = MethodA(methodLocks, 200); + var callB = MethodB(methodLocks, 200); var completed = await Task.WhenAny(Task.WhenAll(callA, callB), Task.Delay(500)); @@ -67,44 +67,44 @@ public async Task LockAsync_ShouldNotBlock_DifferentMethods() [Fact] public async Task LockAsync_WithExplicitMethodName_ShouldScopeIndependently() { - using var methodLock = new MethodThreadLock(); + using var methodLocks = new MethodThreadLocks(); - await methodLock.LockAsync("CustomKey1"); + await methodLocks.LockAsync("CustomKey1"); - var otherKeyTask = methodLock.LockAsync("CustomKey2"); + var otherKeyTask = methodLocks.LockAsync("CustomKey2"); var completed = await Task.WhenAny(otherKeyTask, Task.Delay(500)) == otherKeyTask; Assert.True(completed); - methodLock.Unlock("CustomKey1"); - methodLock.Unlock("CustomKey2"); + methodLocks.Unlock("CustomKey1"); + methodLocks.Unlock("CustomKey2"); } [Fact] public async Task LockAsync_ShouldThrowArgumentNullException_WhenMethodNameIsNullOrWhiteSpace() { - using var methodLock = new MethodThreadLock(); + using var methodLocks = new MethodThreadLocks(); - await Assert.ThrowsAsync(() => methodLock.LockAsync(" ")); + await Assert.ThrowsAsync(() => methodLocks.LockAsync(" ")); } [Fact] public void Unlock_ShouldThrowArgumentNullException_WhenMethodNameIsNullOrWhiteSpace() { - using var methodLock = new MethodThreadLock(); + using var methodLocks = new MethodThreadLocks(); - Assert.Throws(() => methodLock.Unlock(" ")); + Assert.Throws(() => methodLocks.Unlock(" ")); } [Fact] public async Task LockAsync_ShouldThrowOperationCanceledException_WhenCancelled() { - using var methodLock = new MethodThreadLock(); + using var methodLocks = new MethodThreadLocks(); - await methodLock.LockAsync(nameof(LockAsync_ShouldThrowOperationCanceledException_WhenCancelled)); + await methodLocks.LockAsync(nameof(LockAsync_ShouldThrowOperationCanceledException_WhenCancelled)); using var cts = new CancellationTokenSource(); - var waitingTask = methodLock.LockAsync(nameof(LockAsync_ShouldThrowOperationCanceledException_WhenCancelled), cts.Token); + var waitingTask = methodLocks.LockAsync(nameof(LockAsync_ShouldThrowOperationCanceledException_WhenCancelled), cts.Token); cts.Cancel(); @@ -114,12 +114,12 @@ public async Task LockAsync_ShouldThrowOperationCanceledException_WhenCancelled( [Fact] public async Task LockAsync_ShouldThrowObjectDisposedException_WhenDisposedWhileWaiting() { - var methodLock = new MethodThreadLock(); + var methodLocks = new MethodThreadLocks(); - await methodLock.LockAsync("SharedMethod"); + await methodLocks.LockAsync("SharedMethod"); - var waitingTask = methodLock.LockAsync("SharedMethod"); - methodLock.Dispose(); + var waitingTask = methodLocks.LockAsync("SharedMethod"); + methodLocks.Dispose(); await Assert.ThrowsAsync(() => waitingTask); } @@ -127,14 +127,14 @@ public async Task LockAsync_ShouldThrowObjectDisposedException_WhenDisposedWhile [Fact] public async Task LockAsync_ShouldOnlyAllowOneCaller_PerMethodName_UnderConcurrentAccess() { - using var methodLock = new MethodThreadLock(); + using var methodLocks = new MethodThreadLocks(); var counter = 0; var maxObservedConcurrency = 0; var gate = new object(); async Task Worker() { - await methodLock.LockAsync(nameof(Worker)); + await methodLocks.LockAsync(nameof(Worker)); try { lock (gate) @@ -152,7 +152,7 @@ async Task Worker() } finally { - methodLock.Unlock(nameof(Worker)); + methodLocks.Unlock(nameof(Worker)); } } diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MultipleThreadLockManagerTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MultipleThreadLockManagerTests.cs index c46c8b4..023696d 100644 --- a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MultipleThreadLockManagerTests.cs +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MultipleThreadLockManagerTests.cs @@ -12,7 +12,7 @@ public class MultipleThreadLockManagerTests [Fact] public async Task LockAsync_ShouldThrowArgumentNullException_WhenKeyIsNull() { - using var manager = new MultipleThreadLockManager(); + using var manager = new MultipleThreadLocks(); await Assert.ThrowsAsync(() => manager.LockAsync(null!)); } @@ -20,7 +20,7 @@ public async Task LockAsync_ShouldThrowArgumentNullException_WhenKeyIsNull() [Fact] public void Unlock_ShouldThrowArgumentNullException_WhenKeyIsNull() { - using var manager = new MultipleThreadLockManager(); + using var manager = new MultipleThreadLocks(); Assert.Throws(() => manager.Unlock(null!)); } @@ -28,7 +28,7 @@ public void Unlock_ShouldThrowArgumentNullException_WhenKeyIsNull() [Fact] public void Unlock_ShouldThrowKeyNotFoundException_WhenKeyDoesNotExist() { - using var manager = new MultipleThreadLockManager(); + using var manager = new MultipleThreadLocks(); Assert.Throws(() => manager.Unlock("missing-key")); } @@ -36,7 +36,7 @@ public void Unlock_ShouldThrowKeyNotFoundException_WhenKeyDoesNotExist() [Fact] public async Task Unlock_ShouldThrowSynchronizationLockException_WhenKeyLockIsNotHeld() { - using var manager = new MultipleThreadLockManager(); + using var manager = new MultipleThreadLocks(); await manager.LockAsync("key1"); manager.Unlock("key1"); @@ -47,7 +47,7 @@ public async Task Unlock_ShouldThrowSynchronizationLockException_WhenKeyLockIsNo [Fact] public async Task LockAsync_ShouldCreateSeparateLocks_ForDifferentKeys() { - using var manager = new MultipleThreadLockManager(); + using var manager = new MultipleThreadLocks(); var lockTask1 = manager.LockAsync("key1"); var lockTask2 = manager.LockAsync("key2"); @@ -61,7 +61,7 @@ public async Task LockAsync_ShouldCreateSeparateLocks_ForDifferentKeys() [Fact] public async Task LockAsync_ShouldBlock_ForSameKey_UntilUnlocked() { - using var manager = new MultipleThreadLockManager(); + using var manager = new MultipleThreadLocks(); await manager.LockAsync("shared-key"); @@ -79,7 +79,7 @@ public async Task LockAsync_ShouldBlock_ForSameKey_UntilUnlocked() [Fact] public async Task LockAsync_ShouldThrowOperationCanceledException_WhenCancelled() { - using var manager = new MultipleThreadLockManager(); + using var manager = new MultipleThreadLocks(); await manager.LockAsync("key1"); @@ -94,7 +94,7 @@ public async Task LockAsync_ShouldThrowOperationCanceledException_WhenCancelled( [Fact] public async Task LockAsync_ShouldThrowObjectDisposedException_WhenManagerIsDisposedWhileWaiting() { - var manager = new MultipleThreadLockManager(); + var manager = new MultipleThreadLocks(); await manager.LockAsync("key1"); @@ -108,7 +108,7 @@ public async Task LockAsync_ShouldThrowObjectDisposedException_WhenManagerIsDisp [Fact] public async Task LockAsync_ShouldThrowObjectDisposedException_WhenAlreadyDisposed() { - var manager = new MultipleThreadLockManager(); + var manager = new MultipleThreadLocks(); manager.Dispose(); await Assert.ThrowsAsync(() => manager.LockAsync("key1")); @@ -117,7 +117,7 @@ public async Task LockAsync_ShouldThrowObjectDisposedException_WhenAlreadyDispos [Fact] public void Unlock_ShouldThrowObjectDisposedException_WhenAlreadyDisposed() { - var manager = new MultipleThreadLockManager(); + var manager = new MultipleThreadLocks(); manager.Dispose(); Assert.Throws(() => manager.Unlock("key1")); @@ -126,7 +126,7 @@ public void Unlock_ShouldThrowObjectDisposedException_WhenAlreadyDisposed() [Fact] public void Dispose_ShouldBeIdempotent() { - var manager = new MultipleThreadLockManager(); + var manager = new MultipleThreadLocks(); manager.Dispose(); var exception = Record.Exception(() => manager.Dispose()); @@ -137,7 +137,7 @@ public void Dispose_ShouldBeIdempotent() [Fact] public async Task LockAsync_ShouldOnlyAllowOneThreadPerKey_UnderConcurrentAccess() { - using var manager = new MultipleThreadLockManager(); + using var manager = new MultipleThreadLocks(); var counter = 0; var maxObservedConcurrency = 0; var gate = new object(); diff --git a/src/ThunderDesign.Net-PCL.Threading.nuspec.in b/src/ThunderDesign.Net-PCL.Threading.nuspec.in index ce4fc7a..41923be 100644 --- a/src/ThunderDesign.Net-PCL.Threading.nuspec.in +++ b/src/ThunderDesign.Net-PCL.Threading.nuspec.in @@ -13,6 +13,7 @@ See ${{ env.REPOSITORY_URL }}/releases/tag/$version$ ${{ env.TAGS }} + icon.png README.md @@ -58,6 +59,9 @@ + + + diff --git a/src/ThunderDesign.Net-PCL.Threading/Extentions/IMethodThreadLocksExtentions.cs b/src/ThunderDesign.Net-PCL.Threading/Extentions/IMethodThreadLocksExtentions.cs new file mode 100644 index 0000000..a534677 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Extentions/IMethodThreadLocksExtentions.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Interfaces; + +namespace ThunderDesign.Net.Threading.Extentions +{ + public static class IMethodThreadLocksExtentions + { + public static async Task WrapWithLockAsync(this IMethodThreadLocks methodThreadLocks, Func action, [CallerMemberName] string? methodName = null, CancellationToken cancellationToken = default) + { + if (methodThreadLocks == null) + throw new ArgumentNullException(nameof(methodThreadLocks)); + + if (action == null) + throw new ArgumentNullException(nameof(action)); + + if (string.IsNullOrWhiteSpace(methodName)) + throw new ArgumentNullException(nameof(methodName)); + + await methodThreadLocks.LockAsync(methodName, cancellationToken).ConfigureAwait(false); + try + { + await action().ConfigureAwait(false); + } + finally + { + methodThreadLocks.Unlock(methodName); + } + } + + public static async Task WrapWithLockAsync(this IMethodThreadLocks methodThreadLocks, Func> action, [CallerMemberName] string? methodName = null, CancellationToken cancellationToken = default) + { + if (methodThreadLocks == null) + throw new ArgumentNullException(nameof(methodThreadLocks)); + + if (action == null) + throw new ArgumentNullException(nameof(action)); + + if (string.IsNullOrWhiteSpace(methodName)) + throw new ArgumentNullException(nameof(methodName)); + + await methodThreadLocks.LockAsync(methodName, cancellationToken).ConfigureAwait(false); + try + { + return await action().ConfigureAwait(false); + } + finally + { + methodThreadLocks.Unlock(methodName); + } + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Extentions/ISingleThreadLockExtentions.cs b/src/ThunderDesign.Net-PCL.Threading/Extentions/ISingleThreadLockExtentions.cs new file mode 100644 index 0000000..0beda7b --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Extentions/ISingleThreadLockExtentions.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Interfaces; + +namespace ThunderDesign.Net.Threading.Extentions +{ + public static class ISingleThreadLockExtentions + { + public static async Task WrapWithLockAsync(this ISingleThreadLock threadLocks, Func action, CancellationToken cancellationToken = default) + { + if (threadLocks == null) + throw new ArgumentNullException(nameof(threadLocks)); + + if (action == null) + throw new ArgumentNullException(nameof(action)); + + await threadLocks.LockAsync(cancellationToken).ConfigureAwait(false); + try + { + await action().ConfigureAwait(false); + } + finally + { + threadLocks.Unlock(); + } + } + + public static async Task WrapWithLockAsync(this ISingleThreadLock threadLocks, Func> action, CancellationToken cancellationToken = default) + { + if (threadLocks == null) + throw new ArgumentNullException(nameof(threadLocks)); + + if (action == null) + throw new ArgumentNullException(nameof(action)); + + await threadLocks.LockAsync(cancellationToken).ConfigureAwait(false); + try + { + return await action().ConfigureAwait(false); + } + finally + { + threadLocks.Unlock(); + } + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IGateKeeper.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IGateKeeper.cs index af5e7c9..e08fda2 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IGateKeeper.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IGateKeeper.cs @@ -19,7 +19,7 @@ public interface IGateKeeper : IDisposable public interface IGateKeeper : IGateKeeper where TKey : notnull where TValue : IGatedThreadLock { - IGatedThreadLock GetOrAdd(TKey key); + TValue GetOrAdd(TKey key); void Remove(TKey key); } diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMethodThreadLock.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMethodThreadLocks.cs similarity index 89% rename from src/ThunderDesign.Net-PCL.Threading/Interfaces/IMethodThreadLock.cs rename to src/ThunderDesign.Net-PCL.Threading/Interfaces/IMethodThreadLocks.cs index b01f34e..6ea3fe8 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMethodThreadLock.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMethodThreadLocks.cs @@ -8,7 +8,7 @@ namespace ThunderDesign.Net.Threading.Interfaces { - public interface IMethodThreadLock : IDisposable + public interface IMethodThreadLocks : IDisposable { Task LockAsync([CallerMemberName] string? methodName = null, CancellationToken cancellationToken = default); diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMultipleThreadLockManager.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMultipleThreadLocks.cs similarity index 75% rename from src/ThunderDesign.Net-PCL.Threading/Interfaces/IMultipleThreadLockManager.cs rename to src/ThunderDesign.Net-PCL.Threading/Interfaces/IMultipleThreadLocks.cs index cc6a84b..c98f2d8 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMultipleThreadLockManager.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMultipleThreadLocks.cs @@ -6,7 +6,7 @@ namespace ThunderDesign.Net.Threading.Interfaces { - public interface IMultipleThreadLockManager : IDisposable where TKey : notnull + public interface IMultipleThreadLocks : IDisposable where TKey : notnull { Task LockAsync(TKey key, CancellationToken cancellationToken = default); diff --git a/src/ThunderDesign.Net-PCL.Threading/Locks/GateKeeper.cs b/src/ThunderDesign.Net-PCL.Threading/Locks/GateKeeper.cs index 93188b0..ff672dc 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Locks/GateKeeper.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Locks/GateKeeper.cs @@ -111,7 +111,7 @@ public async Task CloseGateAsync(CancellationToken cancellationToken = default) } } - public IGatedThreadLock GetOrAdd(TKey key) + public TValue GetOrAdd(TKey key) { ThrowIfDisposed(); diff --git a/src/ThunderDesign.Net-PCL.Threading/Locks/MethodThreadLock.cs b/src/ThunderDesign.Net-PCL.Threading/Locks/MethodThreadLocks.cs similarity index 88% rename from src/ThunderDesign.Net-PCL.Threading/Locks/MethodThreadLock.cs rename to src/ThunderDesign.Net-PCL.Threading/Locks/MethodThreadLocks.cs index e0124a2..002e3f9 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Locks/MethodThreadLock.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Locks/MethodThreadLocks.cs @@ -8,7 +8,7 @@ namespace ThunderDesign.Net.Threading.Locks { - public class MethodThreadLock : MultipleThreadLockManager, IMethodThreadLock, IDisposable + public class MethodThreadLocks : MultipleThreadLocks, IMethodThreadLocks, IDisposable { public override async Task LockAsync([CallerMemberName] string? methodName = null, CancellationToken cancellationToken = default) { diff --git a/src/ThunderDesign.Net-PCL.Threading/Locks/MultipleThreadLockManager.cs b/src/ThunderDesign.Net-PCL.Threading/Locks/MultipleThreadLocks.cs similarity index 93% rename from src/ThunderDesign.Net-PCL.Threading/Locks/MultipleThreadLockManager.cs rename to src/ThunderDesign.Net-PCL.Threading/Locks/MultipleThreadLocks.cs index b05841f..626616d 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Locks/MultipleThreadLockManager.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Locks/MultipleThreadLocks.cs @@ -10,7 +10,7 @@ namespace ThunderDesign.Net.Threading.Locks { - public class MultipleThreadLockManager : DisposableThreadObject, IMultipleThreadLockManager where TKey : notnull where TValue : ISingleThreadLock, new() + public class MultipleThreadLocks : DisposableThreadObject, IMultipleThreadLocks where TKey : notnull where TValue : ISingleThreadLock, new() { protected readonly DictionaryThreadSafe ThreadLocks = new DictionaryThreadSafe(); diff --git a/src/ThunderDesign.Net-PCL.Threading/ThunderDesign.Net-PCL.Threading.csproj b/src/ThunderDesign.Net-PCL.Threading/ThunderDesign.Net-PCL.Threading.csproj index 43c7f17..5ca7e90 100644 --- a/src/ThunderDesign.Net-PCL.Threading/ThunderDesign.Net-PCL.Threading.csproj +++ b/src/ThunderDesign.Net-PCL.Threading/ThunderDesign.Net-PCL.Threading.csproj @@ -10,6 +10,12 @@ $(NoWarn);1903 + + + True + \ + + True diff --git a/src/icon.png b/src/icon.png new file mode 100644 index 0000000..1b27667 Binary files /dev/null and b/src/icon.png differ