From 9b82de56c985f37cbf3890d6db65ed148002607e Mon Sep 17 00:00:00 2001 From: Shawn LaMountain Date: Wed, 15 Jul 2026 13:32:52 -0400 Subject: [PATCH 1/2] Renamed MethodThreadLock to MethodThreadLocks. Renamed MultipleThreadLockManager to MultipleThreadLocks. Gatekeeper.GetOrAdd now returns TValue instead of IGatedThreadLock. Added Extentions to wrap an action with a Lock. --- ...LockTests.cs => MethodThreadLocksTests.cs} | 64 +++++++++--------- .../Locks/MultipleThreadLockManagerTests.cs | 24 +++---- src/ThunderDesign.Net-PCL.Threading.nuspec.in | 4 ++ .../IMethodThreadLocksExtentions.cs | 58 ++++++++++++++++ .../Extentions/ISingleThreadLockExtentions.cs | 48 +++++++++++++ .../Interfaces/IGateKeeper.cs | 2 +- ...hodThreadLock.cs => IMethodThreadLocks.cs} | 2 +- ...LockManager.cs => IMultipleThreadLocks.cs} | 2 +- .../Locks/GateKeeper.cs | 2 +- ...thodThreadLock.cs => MethodThreadLocks.cs} | 2 +- ...dLockManager.cs => MultipleThreadLocks.cs} | 2 +- .../ThunderDesign.Net-PCL.Threading.csproj | 6 ++ src/icon.png | Bin 0 -> 7502 bytes 13 files changed, 166 insertions(+), 50 deletions(-) rename src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/{MethodThreadLockTests.cs => MethodThreadLocksTests.cs} (64%) create mode 100644 src/ThunderDesign.Net-PCL.Threading/Extentions/IMethodThreadLocksExtentions.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading/Extentions/ISingleThreadLockExtentions.cs rename src/ThunderDesign.Net-PCL.Threading/Interfaces/{IMethodThreadLock.cs => IMethodThreadLocks.cs} (89%) rename src/ThunderDesign.Net-PCL.Threading/Interfaces/{IMultipleThreadLockManager.cs => IMultipleThreadLocks.cs} (75%) rename src/ThunderDesign.Net-PCL.Threading/Locks/{MethodThreadLock.cs => MethodThreadLocks.cs} (88%) rename src/ThunderDesign.Net-PCL.Threading/Locks/{MultipleThreadLockManager.cs => MultipleThreadLocks.cs} (93%) create mode 100644 src/icon.png 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 0000000000000000000000000000000000000000..1b27667eb9bac86f2398db6fb8ff275482a6ccad GIT binary patch literal 7502 zcmbW6hcg_``^Qf&Aw={}Nc3J$OGJ;};zX}O^d6lcdYmA4TC|hXd+&sB(Q@Jl=Ws;y z-rLXTPxyUz=GmQ{-Pzfh-RHIMXP*RpU3GF&Mp6I(K(48wVtC(1{%6F5_w|6HE#khx z@ikOe22_tTZ`^nAonPp@008RJ9$ecJ-1kYmG(f%pz{CFk42OnCD-r-;#%ij(Fb=ZX z$qNp$FhQRyoh{B~j0k}Kz}%sJuXw9I)8aUWsJ@`&O|pBT`oiE5YM8(v)MUYmjpmGw z?wDp8RZ!_(XvkyvOG;;>yG#e#r0FE4`LqE8X`8>enSx~*&{`jdjr|PGU%&RjB)lph&8+ z|7F;Udnzc`|4oa}@ws=Y*SrHvpcf$`FQ(-gq15&ArF?UMU{Bcblyl(TyfXm9FCYg> zAC-=~mQ)AI&4ui?h& zfMv3&Q;?eUaf+Hj1D{n*8eah$C_SBq=?J!1Ej#y^14pS?3h)KqC z16+e=E%>hE3y&7gukz8T{P22>2mQrQn5HeNyUXiwJJ5Y^9q_Xe30xyzV6l#nVfmy; zPTv8V*P%q`cD9xOEp%HIJrWZP0aOa0-Km&}fFaM*dM z6`7!(R8>kgFFc-Xe@EEas8o~<&^79R>_XLw=8FfWc(#jKAPl z2Gj|WLkNZ0w#wwmaMAj#CR-)mH2hX+6ppcUJ?&pfj67$$v!;|Sx4)z!S-%l`z`MwYG@49~sF zS}HJ1s%eM9PmmDtK%pjo$>iZ7&D3W>S2VAej^48gCyRo36CSZcwfB$I^J}|mj|dci z^3j|lJc-JVCNxKMLJl+APk(`EY)Q)9$~C#DW_NTy6h3BSdXhaRq6i$ybvaqM8A&6F zM_Wl)=YlD}cJ&mnuvmr=5fP0y@lu7?PQMhw?ZI0cfl)l|Pc z1=r5jx%1QIH+;ycc{BdG{ZCGmI;p+3X`)qR%rKCCe2w;p(8QDqpCbLtO>eTr{j{ zc#{w8wF44OBh5HRgjs_twS0h78xDluZPlgyw*v^W2Q-1DoUg?CHre6YgdGFA5 zK?mpl?%q51j*tD@V4rd=`F!^5F(YYr(x^?>^6T)>C_Z^zx-hf?v!~K%>?>N|lN@2^ z+g#Z*9Bck$0|Dwhz=1Ke%)G@C!$w-A&4pa82N|1iQxY#V&g*kQpipya5J!g5!Nm=` zrr(*L-KvBgR`k|=&d}qHcLo54lipANUB!UplYvS8pUzA&&gi1)i))+@l(P=8p^9nM zZf3ApI8zCdw|`k=#DO$>SQS}GIK){@77%!_6SC{1@TGinank8y+sYZ{ju%*Iy_YLj1%ggtC{CxDJu|M1xVZ2|Y-1*4K z8z{lgm7U}noG+6%@ZDS=X*eJm-d|l#)-!Cny3TbtQv}4vT=+r<-K7`ze-rqwhZl>y z6&0P!jwYg$l%BsaF%j(EWUnTbu160&m-*lXId`jomsbzfBeZj5V?akvNQuCQG*-e; z7!r*fSUwFfgmZz5zW%M3$C+L?!ZO zH9jO+waEA{|0*?!^-hY_6b3{(g5J5h$@w&v1=>m!*Dv`e9T4Gu$x48ZJ$l7OpQ`f3 z&?sei=P6?fSfk|Iw{I%x+#k5VRl&In#;6{~Ck$h`9(%7~vBqtAq1UJLthuP!^JUVs zGLhqwG9G-Hn3~2wzwlsz%w~!qZ1T|K^U!cIgvyxHfpIJR*wh9@3u#P5E)P02JWBiS zEyK)(^}c^ers_obR&fExd~JvUgO5P4BYBGR`#))BxK=8BMt zkF_xmQ!rOdtHrnKp=&{;K*zv{H2^(RlARH?PaAUsBjdh_F*{jIUms3xR$t0%YaQ%Q zh;13NjhbGFObWtliZiJbGd2tykS&34IDmiHe7uL)CQSExz4pooe!kJmcAYxg3`cMR zJ{0zdfj%;KG`h|AVkrcZ((Ya`lj7p! z)Kojt7xwme;xH5*Wm0NPFSh6XXEh=j!8aXWC~;WK4oGVH2sziLf{f@j+?*uVG9gLwh9 zM8il6EBl=p(}3m{rhpus`(rn29}n_Ne*8?=#=&>NZ|t|l`{mf_nVF%;;<EFiXI^1UDNH@?a~QQ}szC zH+}`m(_w5#$GW0G@ung`^>1%?*bm(5aHM1C61wHvbN4!>i=(|#9()n<3`xNp283JB z*2Z4fZ!y6*A|iN|SHtya%VB#1U8-?#7abrh*8f#r5#`c>^GyBCQs17e#qI=oT-+y} zj91x$SW86B_cx#mmZev{tC*<>+zhM8l=o171a0@a_`&XE%5YqC{(2O z+qB-HRe~meZ;<@>A9AL9V(fzaWHBlOX0-z)ufwgAIFpWGN*PZd0oqRu^ zNJD2r6f++sO`FAgG^%Zmqm5KC_)HWT)*BN$kLU#6Q`7NQRQvP}KPxyz`#p|?PO*IA z%>=I=4)-`B70~Wdw8;pg{MIHPJizV=@~7K4ys>+W(8f7@+E7*p&(cey1izwvG~X($V?Fr-M!j& zw%b9Y&)7UX?~W7K5P^7Au{+n|8w`vq36gH|Z02<&FZx-*MF6f#YEvSl;v7ZgY0)0gznlH;H8ePlyMGkQ z{E@K1AfC|u-fld(GZs16?mMMRZL!SDcm?6xpa>n(8sfK)?0;cOusd(ybw^xn~O|eLghzr3DR0Br2aMoZQTJArr%ke>jgd<#5 z9@u`_#?nz~vL*ecz50rDs}t%#&0NqOh0}eLQNXP8G^4G*zQ~5n$~8h8aaqy%I>*Wf zUnZ6yl(uycyq2>9r*5;@E$dB-LgB$$>Z7%K(gx7zL*##s6?m7MAJs?K?i5)b;$bJ4Z zwvh6f+7k^iSxvC6^@>h8NNNu~jGX^78{UY{(d5?d zf7U*wHyZh3{CT=#3vwB-kIM!zH1b7nO_(oz9JC)w4ZE`TOW~kSsrR0+%Ac#tIK92? z*$kP`|M)wnrMA4OsoWnE9$|$9>|cbPhjqtCr6<(m&LV%0Am>{S79PmO3=_pURhzd& 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 PfdVvDbycdBZKD4V$o#&V literal 0 HcmV?d00001 From e6268d9eec292c5e996b1a1a9601f5c8ca8355b2 Mon Sep 17 00:00:00 2001 From: Shawn LaMountain Date: Wed, 15 Jul 2026 14:22:15 -0400 Subject: [PATCH 2/2] Updated README --- README.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) 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`. ----