From 671ca4046c1b657b3191e2706c605c3c5fe2ffb3 Mon Sep 17 00:00:00 2001 From: Shawn LaMountain Date: Tue, 14 Jul 2026 02:50:11 -0400 Subject: [PATCH] Added new Lock classes --- .../UnifiedPropertyGenerator.cs | 20 +- .../Attributes/BindablePropertyAttribute.cs | 8 +- .../Attributes/PropertyAttribute.cs | 4 +- .../Extentions/IBindableObjectExtention.cs | 2 +- .../INotifyPropertyChangedExtension.cs | 10 +- .../Extentions/ObjectExtention.cs | 10 +- .../HelperClasses/ThreadHelper.cs | 2 +- .../Objects/DisposableThreadObject.cs | 78 ++++++ .../Objects/ThreadObject.cs | 2 +- ...nderDesign.Net-PCL.Threading.Shared.csproj | 10 +- .../CollectionThreadSafeApiParityTests.cs | 17 ++ .../DictionaryThreadSafeApiParityTests.cs | 17 ++ .../HashSetThreadSafeApiParityTests.cs | 18 ++ .../IsSynchronizedApiParityTests.cs | 120 +++++++++ .../LinkedListThreadSafeApiParityTests.cs | 17 ++ .../ListThreadSafeApiParityTests.cs | 17 ++ ...vableCollectionThreadSafeApiParityTests.cs | 37 +++ ...vableDictionaryThreadSafeApiParityTests.cs | 49 ++++ .../QueueThreadSafeApiParityTests.cs | 17 ++ ...ortedDictionaryThreadSafeApiParityTests.cs | 17 ++ .../SortedListThreadSafeApiParityTests.cs | 17 ++ .../StackThreadSafeApiParityTests.cs | 17 ++ .../ObservableDataCollectionApiParityTests.cs | 21 ++ .../ObservableDataDictionaryApiParityTests.cs | 20 ++ .../Helpers/ApiParityHelper.cs | 132 ++++++++++ .../Locks/GateKeeperTests.cs | 207 +++++++++++++++ .../Locks/GatedThreadLockTests.cs | 117 +++++++++ .../Locks/MethodThreadLockTests.cs | 168 ++++++++++++ .../Locks/MultipleThreadLockManagerTests.cs | 178 +++++++++++++ .../Locks/SingleThreadLockTests.cs | 175 ++++++++++++ ...rDesign.Net-PCL.Threading.UnitTests.csproj | 27 ++ src/ThunderDesign.Net-PCL.Threading.nuspec.in | 6 +- src/ThunderDesign.Net-PCL.Threading.sln | 10 +- .../Collections/CollectionThreadSafe.cs | 10 +- .../Collections/DictionaryThreadSafe.cs | 133 +++++++--- .../Collections/HashSetThreadSafe.cs | 132 +++++++++- .../Collections/LinkedListThreadSafe.cs | 52 +++- .../Collections/ListThreadSafe.cs | 25 +- .../ObservableCollectionThreadSafe.cs | 23 +- .../ObservableDictionaryThreadSafe.cs | 75 ++---- .../Collections/QueueThreadSafe.cs | 45 +++- .../Collections/SortedDictionaryThreadSafe.cs | 31 ++- .../Collections/SortedListThreadSafe.cs | 57 +++- .../Collections/StackThreadSafe.cs | 56 +++- .../ObservableDataDictionary.cs | 2 +- .../DataObjects/BindableDataObject.cs | 6 +- .../DataObjects/DataObject.cs | 12 +- .../INotifyCollectionChangedExtension.cs | 2 +- .../Interfaces/IBindableDataObject.cs | 2 +- .../Interfaces/IDataObject.cs | 4 +- .../Interfaces/IDictionaryThreadSafe.cs | 7 +- .../Interfaces/IGateKeeper.cs | 26 ++ .../Interfaces/IGatedThreadLock.cs | 10 + .../Interfaces/IHashSetThreadSafe.cs | 11 +- .../Interfaces/ILinkedListThreadSafe.cs | 8 +- .../Interfaces/IMethodThreadLock.cs | 17 ++ .../Interfaces/IMultipleThreadLockManager.cs | 15 ++ .../Interfaces/IObservableDataDictionary.cs | 2 +- .../IObservableDictionaryThreadSafe.cs | 2 +- .../Interfaces/ISingleThreadLock.cs | 13 + .../Interfaces/ISortedDictionaryThreadSafe.cs | 10 +- .../Interfaces/ISortedListThreadSafe.cs | 2 +- .../Interfaces/IStackThreadSafe.cs | 4 +- .../Locks/GateKeeper.cs | 248 ++++++++++++++++++ .../Locks/GatedThreadLock.cs | 88 +++++++ .../Locks/MethodThreadLock.cs | 29 ++ .../Locks/MultipleThreadLockManager.cs | 84 ++++++ .../Locks/SingleThreadLock.cs | 94 +++++++ .../Objects/BindableObject.cs | 2 +- .../ThunderDesign.Net-PCL.Threading.csproj | 14 +- 70 files changed, 2736 insertions(+), 184 deletions(-) create mode 100644 src/ThunderDesign.Net-PCL.Threading.Shared/Objects/DisposableThreadObject.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/CollectionThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/DictionaryThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/HashSetThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/IsSynchronizedApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/LinkedListThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ListThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ObservableCollectionThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ObservableDictionaryThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/QueueThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/SortedDictionaryThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/SortedListThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/StackThreadSafeApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/DataCollections/ObservableDataCollectionApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/DataCollections/ObservableDataDictionaryApiParityTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Helpers/ApiParityHelper.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/GateKeeperTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/GatedThreadLockTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MethodThreadLockTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MultipleThreadLockManagerTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/SingleThreadLockTests.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading.UnitTests/ThunderDesign.Net-PCL.Threading.UnitTests.csproj create mode 100644 src/ThunderDesign.Net-PCL.Threading/Interfaces/IGateKeeper.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading/Interfaces/IGatedThreadLock.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading/Interfaces/IMethodThreadLock.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading/Interfaces/IMultipleThreadLockManager.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading/Interfaces/ISingleThreadLock.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading/Locks/GateKeeper.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading/Locks/GatedThreadLock.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading/Locks/MethodThreadLock.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading/Locks/MultipleThreadLockManager.cs create mode 100644 src/ThunderDesign.Net-PCL.Threading/Locks/SingleThreadLock.cs diff --git a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs index b2ca412..ff1b3d7 100644 --- a/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs +++ b/src/ThunderDesign.Net-PCL.SourceGenerators/UnifiedPropertyGenerator.cs @@ -188,6 +188,7 @@ private static void GenerateUnifiedPropertyClass( GenerateRegularProperties(source, propertyFields, classSymbol, compilation); source.AppendLine("}"); + source.AppendLine("#nullable restore"); if (!string.IsNullOrEmpty(classSymbol.ContainingNamespace?.ToDisplayString())) source.AppendLine("}"); @@ -512,7 +513,7 @@ private static void GenerateInfrastructureMembers( if (bindableFields.Count > 0 && !implementsINotify && !PropertyGeneratorHelpers.EventExists(classSymbol, "PropertyChanged", propertyChangedEventType)) { - source.AppendLine(" public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); + source.AppendLine(" public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;"); } // Add _Locker if needed (only for non-static fields) @@ -1096,6 +1097,7 @@ public static T GetStaticProperty( { if (lockObj != null) System.Threading.Monitor.Enter(lockObj, ref lockWasTaken); + return backingStore; } finally @@ -1128,21 +1130,23 @@ public static bool SetStaticProperty( { if (lockObj != null) System.Threading.Monitor.Enter(lockObj, ref lockWasTaken); + +#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER + if (ThunderDesign.Net_PCL.ToolBox.Helpers.EqualityHelper.Equality(ref backingStore, value)) + return false; +#else if (System.Collections.Generic.EqualityComparer.Default.Equals(backingStore, value)) - { return false; - } - else - { - backingStore = value; - return true; - } +#endif + backingStore = value; } finally { if (lockWasTaken) System.Threading.Monitor.Exit(lockObj!); } + + return true; }"); } } diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs index 0997a87..05c86bd 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs @@ -11,16 +11,16 @@ public sealed class BindablePropertyAttribute : Attribute { public bool ThreadSafe { get; } public bool Notify { get; } - public string[] AlsoNotify { get; } - public string CallMethodAfterSet { get; } + public string[]? AlsoNotify { get; } + public string? CallMethodAfterSet { get; } public AccessorAccessibility Getter { get; } public AccessorAccessibility Setter { get; } public BindablePropertyAttribute( bool threadSafe = true, bool notify = true, - string[] alsoNotify = null, - string callMethodAfterSet = null, + string[]? alsoNotify = null, + string? callMethodAfterSet = null, AccessorAccessibility getter = AccessorAccessibility.Public, AccessorAccessibility setter = AccessorAccessibility.Public) { diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs index 4c06353..6ce30ab 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs @@ -8,13 +8,13 @@ namespace ThunderDesign.Net.Threading.Attributes public sealed class PropertyAttribute : Attribute { public bool ThreadSafe { get; } - public string CallMethodAfterSet { get; } + public string? CallMethodAfterSet { get; } public AccessorAccessibility Getter { get; } public AccessorAccessibility Setter { get; } public PropertyAttribute( bool threadSafe = true, - string callMethodAfterSet = null, + string? callMethodAfterSet = null, AccessorAccessibility getter = AccessorAccessibility.Public, AccessorAccessibility setter = AccessorAccessibility.Public) { diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/IBindableObjectExtention.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/IBindableObjectExtention.cs index 6de1b88..901aec6 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/IBindableObjectExtention.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/IBindableObjectExtention.cs @@ -19,7 +19,7 @@ public static bool SetProperty( this IBindableObject sender, ref T backingStore, T value, - object lockObj, + object? lockObj, bool notifyPropertyChanged, [CallerMemberName] string propertyName = "") { diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/INotifyPropertyChangedExtension.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/INotifyPropertyChangedExtension.cs index ae88cdb..ae27ee5 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/INotifyPropertyChangedExtension.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/INotifyPropertyChangedExtension.cs @@ -9,7 +9,7 @@ public static class INotifyPropertyChangedExtension { public static void NotifyPropertyChanged( this INotifyPropertyChanged sender, - PropertyChangedEventHandler handler, + PropertyChangedEventHandler? handler, [CallerMemberName] string propertyName = "", bool notifyAndWait = true) { @@ -18,7 +18,7 @@ public static void NotifyPropertyChanged( public static void NotifyPropertyChanged( this INotifyPropertyChanged sender, - PropertyChangedEventHandler handler, + PropertyChangedEventHandler? handler, PropertyChangedEventArgs args, bool notifyAndWait = true) { @@ -35,7 +35,7 @@ public static bool SetProperty( this INotifyPropertyChanged sender, ref T backingStore, T value, - PropertyChangedEventHandler propertyChangedEventHandler, + PropertyChangedEventHandler? propertyChangedEventHandler, [CallerMemberName] string propertyName = "", bool notifyAndWait = true) { @@ -46,8 +46,8 @@ public static bool SetProperty( this INotifyPropertyChanged sender, ref T backingStore, T value, - object lockObj, - PropertyChangedEventHandler propertyChangedEventHandler, + object? lockObj, + PropertyChangedEventHandler? propertyChangedEventHandler, [CallerMemberName] string propertyName = "", bool notifyAndWait = true) { diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/ObjectExtention.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/ObjectExtention.cs index 98e5575..654d56c 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/ObjectExtention.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Extentions/ObjectExtention.cs @@ -10,19 +10,20 @@ public static class ObjectExtention public static T GetProperty( this object sender, ref T backingStore, - object lockObj = null) + object? lockObj = null) { bool lockWasTaken = false; try { if (lockObj != null) System.Threading.Monitor.Enter(lockObj, ref lockWasTaken); + return backingStore; } finally { if (lockWasTaken) - System.Threading.Monitor.Exit(lockObj); + System.Threading.Monitor.Exit(lockObj!); } } @@ -30,7 +31,7 @@ public static bool SetProperty( this object sender, ref T backingStore, T value, - object lockObj = null) + object? lockObj = null) { bool lockWasTaken = false; try @@ -50,8 +51,9 @@ public static bool SetProperty( finally { if (lockWasTaken) - System.Threading.Monitor.Exit(lockObj); + System.Threading.Monitor.Exit(lockObj!); } + return true; } } diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/HelperClasses/ThreadHelper.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/HelperClasses/ThreadHelper.cs index 24a5862..a136520 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/HelperClasses/ThreadHelper.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/HelperClasses/ThreadHelper.cs @@ -11,7 +11,7 @@ public static class ThreadHelper catch { } }; - public static void RunAndForget(Action action, Action handler = null) + public static void RunAndForget(Action action, Action? handler = null) { if (action == null) throw new ArgumentNullException(nameof(action)); diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Objects/DisposableThreadObject.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Objects/DisposableThreadObject.cs new file mode 100644 index 0000000..c852512 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Objects/DisposableThreadObject.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using ThunderDesign.Net.Threading.Extentions; + +namespace ThunderDesign.Net.Threading.Objects +{ + public abstract class DisposableThreadObject : ThreadObject, IDisposable + { + private CancellationTokenSource? _disposingCtsCached = null; + + protected readonly string ClassNameCashed; + private bool _disposed = false; + + protected DisposableThreadObject() + { + ClassNameCashed = this.GetType().Name; + } + + ~DisposableThreadObject() + { + Dispose(disposing: false); + } + + protected bool Disposed + { + get { return this.GetProperty(ref _disposed, _Locker); } + set { this.SetProperty(ref _disposed, value, _Locker); } + } + + protected CancellationTokenSource DisposingCts => DisposingCtsCache ??= new CancellationTokenSource(); + + protected CancellationTokenSource? DisposingCtsCache + { + get { return this.GetProperty(ref _disposingCtsCached, _Locker); } + private set { this.SetProperty(ref _disposingCtsCached, value, _Locker); } + } + + protected CancellationTokenSource LinkTokenSourceDisposing(CancellationToken cancellationToken) + { + return CancellationTokenSource.CreateLinkedTokenSource(DisposingCts.Token, cancellationToken); + } + + protected void ThrowIfDisposed() + { + if (Disposed || DisposingCtsCache?.IsCancellationRequested == true) + throw new ObjectDisposedException(ClassNameCashed); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing, bool setDisposed = true) + { + if (!Disposed) + { + if (disposing) + { + // Dispose managed resources here + DisposingCtsCache?.Cancel(); + DisposingCtsCache?.Dispose(); + } + + // Dispose unmanaged resources here + + if (setDisposed) + { + Disposed = true; + } + } + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/Objects/ThreadObject.cs b/src/ThunderDesign.Net-PCL.Threading.Shared/Objects/ThreadObject.cs index 131001d..68b23bb 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/Objects/ThreadObject.cs +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/Objects/ThreadObject.cs @@ -1,6 +1,6 @@ namespace ThunderDesign.Net.Threading.Objects { - public class ThreadObject + public abstract class ThreadObject { #region variables protected readonly object _Locker = new object(); diff --git a/src/ThunderDesign.Net-PCL.Threading.Shared/ThunderDesign.Net-PCL.Threading.Shared.csproj b/src/ThunderDesign.Net-PCL.Threading.Shared/ThunderDesign.Net-PCL.Threading.Shared.csproj index f17db6f..162515e 100644 --- a/src/ThunderDesign.Net-PCL.Threading.Shared/ThunderDesign.Net-PCL.Threading.Shared.csproj +++ b/src/ThunderDesign.Net-PCL.Threading.Shared/ThunderDesign.Net-PCL.Threading.Shared.csproj @@ -1,8 +1,10 @@  - netstandard1.0;netstandard1.3;netstandard2.0;net461;net6.0;net8.0;net10.0 - ThunderDesign.Net.Threading + netstandard1.0;netstandard1.3;netstandard2.0;net6.0;net8.0;net9.0;net10.0 + latest + enable + ThunderDesign.Net.Threading true true true @@ -11,6 +13,10 @@ Threading interfaces and utilities for ThunderDesign cross-platform libraries. + + $(NoWarn);1903 + + diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/CollectionThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/CollectionThreadSafeApiParityTests.cs new file mode 100644 index 0000000..c1f5a6c --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/CollectionThreadSafeApiParityTests.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class CollectionThreadSafeApiParityTests + { + [Fact] + public void ApiParity_CollectionThreadSafe_CoversCollectionOfT() + { + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(CollectionThreadSafe), + typeof(Collection)); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/DictionaryThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/DictionaryThreadSafeApiParityTests.cs new file mode 100644 index 0000000..4562b9e --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/DictionaryThreadSafeApiParityTests.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class DictionaryThreadSafeApiParityTests + { + [Fact] + public void ApiParity_DictionaryThreadSafe_CoversDictionaryOfTKeyTValue() + { + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(DictionaryThreadSafe), + typeof(Dictionary)); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/HashSetThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/HashSetThreadSafeApiParityTests.cs new file mode 100644 index 0000000..426af3d --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/HashSetThreadSafeApiParityTests.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class HashSetThreadSafeApiParityTests + { + [Fact] + public void ApiParity_HashSetThreadSafe_CoversHashSetOfT() + { + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(HashSetThreadSafe), + typeof(HashSet)); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/IsSynchronizedApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/IsSynchronizedApiParityTests.cs new file mode 100644 index 0000000..6ba0967 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/IsSynchronizedApiParityTests.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net.Threading.DataCollections; +using ThunderDesign.Net.Threading.DataObjects; +using Xunit; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + /// + /// Reflection-based test that discovers every public, non-abstract, non-generic-open + /// class in the Collections and DataCollections namespaces that implements the + /// non-generic interface, and asserts that + /// returns true for an instance of each. + /// + public class IsSynchronizedApiParityTests + { + public static IEnumerable CollectionInstanceFactories() + { + yield return new object[] { "CollectionThreadSafe", (Func)(() => new CollectionThreadSafe()) }; + yield return new object[] { "ListThreadSafe", (Func)(() => new ListThreadSafe()) }; + yield return new object[] { "DictionaryThreadSafe", (Func)(() => new DictionaryThreadSafe()) }; + yield return new object[] { "QueueThreadSafe", (Func)(() => new QueueThreadSafe()) }; + yield return new object[] { "StackThreadSafe", (Func)(() => new StackThreadSafe()) }; + yield return new object[] { "SortedListThreadSafe", (Func)(() => new SortedListThreadSafe()) }; + yield return new object[] { "LinkedListThreadSafe", (Func)(() => new LinkedListThreadSafe()) }; + yield return new object[] { "SortedDictionaryThreadSafe", (Func)(() => new SortedDictionaryThreadSafe()) }; + yield return new object[] { "ObservableCollectionThreadSafe", (Func)(() => new ObservableCollectionThreadSafe()) }; + yield return new object[] { "ObservableDictionaryThreadSafe", (Func)(() => new ObservableDictionaryThreadSafe()) }; + yield return new object[] { "ObservableDataCollection>", (Func)(() => new ObservableDataCollection>()) }; + yield return new object[] { "ObservableDataDictionary>", (Func)(() => new ObservableDataDictionary>()) }; + } + + [Theory] + [MemberData(nameof(CollectionInstanceFactories))] + public void ICollection_IsSynchronized_ShouldReturnTrue(string typeName, Func factory) + { + var instance = factory(); + + Assert.True(instance is ICollection, $"'{typeName}' was expected to implement the non-generic ICollection interface."); + + var collection = (ICollection)instance; + + Assert.True(collection.IsSynchronized, $"'{typeName}' should report ICollection.IsSynchronized == true."); + } + + [Theory] + [MemberData(nameof(CollectionInstanceFactories))] + public void Object_IsSynchronized_ShouldReturnTrue(string typeName, Func factory) + { + var instance = factory(); + + Assert.True(instance != null, $"'{typeName}' was expected to be a non-null instance."); + + // 1. Get the property info from the runtime type + PropertyInfo? prop = instance.GetType()?.GetProperty("IsSynchronized"); + if (prop != null) + { + Assert.True(prop.PropertyType == typeof(bool), $"'{typeName}' was expected to have a property named 'IsSynchronized' of type bool."); + + Assert.True(prop.CanRead && !prop.CanWrite, $"'{typeName}' was expected to have a readable property named 'IsSynchronized'."); + + // 2. Read the value from the object instance + object? actualValue = prop.GetValue(instance, null); + + Assert.True(actualValue is bool && (bool)actualValue, $"'{typeName}' should report IsSynchronized == true."); + } + } + + [Fact] + public void AllICollectionImplementations_InCollectionsAndDataCollectionsNamespaces_AreCoveredByThisTest() + { + var coveredTypes = new HashSet( + CollectionInstanceFactories().Select(args => ((Func)args[1])().GetType().GetGenericTypeDefinition() is Type gtd ? gtd : ((Func)args[1])().GetType())); + + var assembly = typeof(CollectionThreadSafe<>).Assembly; + + var candidateTypes = assembly.GetTypes() + .Where(t => t.IsClass && t.IsPublic && !t.IsAbstract) + .Where(t => (t.Namespace == typeof(CollectionThreadSafe<>).Namespace) || (t.Namespace == typeof(ObservableDataCollection<>).Namespace)) + .Where(t => typeof(ICollection).IsAssignableFrom(t) || ImplementsOpenGenericICollectionInterface(t)) + .ToList(); + + var uncovered = candidateTypes + .Where(t => !coveredTypes.Contains(t.IsGenericTypeDefinition ? t : (t.IsGenericType ? t.GetGenericTypeDefinition() : t))) + .Select(t => t.Name) + .ToList(); + + Assert.True(uncovered.Count == 0, + $"The following types implement ICollection but are not covered by {nameof(IsSynchronizedApiParityTests)}: {string.Join(", ", uncovered)}"); + } + + private static bool ImplementsOpenGenericICollectionInterface(Type t) + { + // Non-generic ICollection is inherited transitively via generic base types (e.g. List : ICollection). + // For open generic definitions in this assembly, check a closed instantiation instead. + if (!t.IsGenericTypeDefinition) return false; + + foreach (var candidateArg in new[] { typeof(int), typeof(BindableDataObject) }) + { + try + { + var genericArgs = t.GetGenericArguments().Select(_ => candidateArg).ToArray(); + var closed = t.MakeGenericType(genericArgs); + if (typeof(ICollection).IsAssignableFrom(closed)) + return true; + } + catch + { + // constraint not satisfied by this candidate argument, try next + } + } + + return false; + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/LinkedListThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/LinkedListThreadSafeApiParityTests.cs new file mode 100644 index 0000000..d41f26c --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/LinkedListThreadSafeApiParityTests.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class LinkedListThreadSafeApiParityTests + { + [Fact] + public void ApiParity_LinkedListThreadSafe_CoversLinkedListOfT() + { + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(LinkedListThreadSafe), + typeof(LinkedList)); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ListThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ListThreadSafeApiParityTests.cs new file mode 100644 index 0000000..d7eb5bf --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ListThreadSafeApiParityTests.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class ListThreadSafeApiParityTests + { + [Fact] + public void ApiParity_ListThreadSafe_CoversListOfT() + { + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(ListThreadSafe), + typeof(List)); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ObservableCollectionThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ObservableCollectionThreadSafeApiParityTests.cs new file mode 100644 index 0000000..8a3acf6 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ObservableCollectionThreadSafeApiParityTests.cs @@ -0,0 +1,37 @@ +using System.Collections.ObjectModel; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class ObservableCollectionThreadSafeApiParityTests + { + [Fact] + public void ApiParity_ObservableCollectionThreadSafe_CoversObservableCollectionOfT() + { + string[] knownExclusions = new string[] + { + "Contains(Int32)", + "CopyTo(Int32[], Int32)", + "GetEnumerator()", + "IndexOf(Int32)", + "GetItemByIndex(Int32)", + "CopyTo(Int32[], Int32)", + "Contains(Int32)", + "GetEnumerator()", + "IndexOf(Int32)", + "Boolean IsSynchronized { get; }", + "Int32 Item { get; }", + "Int32 Count { get; }" + }; + + // ObservableCollectionThreadSafe derives from CollectionThreadSafe rather + // than from ObservableCollection directly, but is intended to be its + // thread-safe functional analog, so parity is checked against ObservableCollection. + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(ObservableCollectionThreadSafe), + typeof(CollectionThreadSafe), + knownExclusions); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ObservableDictionaryThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ObservableDictionaryThreadSafeApiParityTests.cs new file mode 100644 index 0000000..078a5ca --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/ObservableDictionaryThreadSafeApiParityTests.cs @@ -0,0 +1,49 @@ +using System.Collections; +using System.Collections.Generic; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class ObservableDictionaryThreadSafeApiParityTests + { + [Fact] + public void ApiParity_ObservableDictionaryThreadSafe_CoversDictionaryOfTKeyTValue() + { + string[] knownExclusions = new string[] + { + "ContainsKey(String)", + "ContainsValue(Int32)", + "GetEnumerator()", + "OnDeserialization(Object)", + "TryGetValue(String, Int32&)", + "EnsureCapacity(Int32)", + "TrimExcess()", + "TrimExcess(Int32)", + "ContainsKey(String)", + "ContainsValue(Int32)", + "GetEnumerator()", + "GetObjectData(SerializationInfo, StreamingContext)", + "TryGetValue(String, Int32&)", + "EnsureCapacity(Int32)", + "TrimExcess()", + "TrimExcess(Int32)", + "Boolean IsSynchronized { get; }", + "IEqualityComparer`1 Comparer { get; }", + "Int32 Count { get; }", + "KeyCollection Keys { get; }", + "ValueCollection Values { get; }", +#if NET9_0_OR_GREATER + "GetAlternateLookup()", + "TryGetAlternateLookup(AlternateLookup`1&)", + "Int32 Capacity { get; }" +#endif + }; + + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(ObservableDictionaryThreadSafe), + typeof(DictionaryThreadSafe), + knownExclusions); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/QueueThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/QueueThreadSafeApiParityTests.cs new file mode 100644 index 0000000..3b9bdc2 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/QueueThreadSafeApiParityTests.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class QueueThreadSafeApiParityTests + { + [Fact] + public void ApiParity_QueueThreadSafe_CoversQueueOfT() + { + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(QueueThreadSafe), + typeof(Queue)); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/SortedDictionaryThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/SortedDictionaryThreadSafeApiParityTests.cs new file mode 100644 index 0000000..784642e --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/SortedDictionaryThreadSafeApiParityTests.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class SortedDictionaryThreadSafeApiParityTests + { + [Fact] + public void ApiParity_SortedDictionaryThreadSafe_CoversSortedDictionaryOfTKeyTValue() + { + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(SortedDictionaryThreadSafe), + typeof(SortedDictionary)); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/SortedListThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/SortedListThreadSafeApiParityTests.cs new file mode 100644 index 0000000..a646b85 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/SortedListThreadSafeApiParityTests.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class SortedListThreadSafeApiParityTests + { + [Fact] + public void ApiParity_SortedListThreadSafe_CoversSortedListOfTKeyTValue() + { + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(SortedListThreadSafe), + typeof(SortedList)); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/StackThreadSafeApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/StackThreadSafeApiParityTests.cs new file mode 100644 index 0000000..ed5252b --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Collections/StackThreadSafeApiParityTests.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Collections +{ + public class StackThreadSafeApiParityTests + { + [Fact] + public void ApiParity_StackThreadSafe_CoversStackOfT() + { + ApiParityHelper.AssertWrapperCoversBaseApi( + typeof(StackThreadSafe), + typeof(Stack)); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/DataCollections/ObservableDataCollectionApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/DataCollections/ObservableDataCollectionApiParityTests.cs new file mode 100644 index 0000000..a343e91 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/DataCollections/ObservableDataCollectionApiParityTests.cs @@ -0,0 +1,21 @@ +using System.Collections.ObjectModel; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net.Threading.DataCollections; +using ThunderDesign.Net.Threading.DataObjects; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.DataCollections +{ + //public class ObservableDataCollectionApiParityTests + //{ + // [Fact] + // public void ApiParity_ObservableDataCollection_CoversObservableCollectionOfT() + // { + // // ObservableDataCollection derives from ObservableCollectionThreadSafe, + // // whose functional BCL analog is ObservableCollection. + // ApiParityHelper.AssertWrapperCoversBaseApi( + // typeof(ObservableDataCollection>), + // typeof(ObservableCollectionThreadSafe>)); + // } + //} +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/DataCollections/ObservableDataDictionaryApiParityTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/DataCollections/ObservableDataDictionaryApiParityTests.cs new file mode 100644 index 0000000..2909cfb --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/DataCollections/ObservableDataDictionaryApiParityTests.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net.Threading.DataCollections; +using ThunderDesign.Net.Threading.DataObjects; +using ThunderDesign.Net_PCL.Threading.UnitTests.Helpers; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.DataCollections +{ + public class ObservableDataDictionaryApiParityTests + { + //[Fact] + //public void ApiParity_ObservableDataDictionary_CoversDictionaryOfTKeyTValue() + //{ + + // ApiParityHelper.AssertWrapperCoversBaseApi( + // typeof(ObservableDataDictionary>), + // typeof(ObservableDictionaryThreadSafe>)); + //} + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Helpers/ApiParityHelper.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Helpers/ApiParityHelper.cs new file mode 100644 index 0000000..eb6582f --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Helpers/ApiParityHelper.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Helpers +{ + /// + /// Reflection-based helper used to assert that a thread-safe wrapper class + /// exposes a public member for every public instance member declared directly + /// on the wrapped BCL base type (for the currently targeted framework). + /// + public static class ApiParityHelper + { + public static void AssertWrapperCoversBaseApi( + Type wrapperType, + Type baseType, + IEnumerable? knownExclusions = null) + { + string[] globalExclusions = ["GetType()", "ToString()", "Equals(Object)", "GetHashCode()"]; + + if (wrapperType is null) throw new ArgumentNullException(nameof(wrapperType)); + if (baseType is null) throw new ArgumentNullException(nameof(baseType)); + + var exclusions = new HashSet(knownExclusions ?? Array.Empty(), StringComparer.Ordinal); + exclusions.UnionWith(globalExclusions); + + var baseMembers = GetAllPublicInstanceMembers(baseType); + var wrapperMembers = GetDeclaredPublicInstanceMembers(wrapperType); + + var missing = new List(); + + foreach (var baseMember in baseMembers) + { + var signature = DescribeMember(baseMember); + + if (exclusions.Contains(baseMember.Name) || exclusions.Contains(signature)) + continue; + + if (!HasMatchingMember(wrapperMembers, baseMember)) + { + missing.Add(signature); + } + //else + //{ + // // Optionally, you could add a check to ensure that the wrapper member has the same return type as the base member. + // // This is not strictly necessary for API parity, but it can help catch subtle issues. + // var wrapperMember = wrapperMembers.FirstOrDefault(wm => wm.Name == baseMember.Name); + // if (wrapperMember != null && wrapperMember is MethodInfo wrapperMethod && baseMember is MethodInfo baseMethod) + // { + // if (wrapperMethod.ReturnType != baseMethod.ReturnType) + // { + // missing.Add($"{signature} (return type mismatch: expected {baseMethod.ReturnType.Name}, found {wrapperMethod.ReturnType.Name})"); + // } + // } + //} + } + + if (missing.Count > 0) + { + Assert.Fail( + $"'{wrapperType.Name}' is missing wrapper members for the following public API declared on '{baseType.Name}' " + + $"(target framework: {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}):{Environment.NewLine}" + + string.Join(Environment.NewLine, missing.Select(m => " - " + m))); + } + } + + private static List GetDeclaredPublicInstanceMembers(Type baseType) + { + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + + var methods = baseType.GetMethods(flags) + .Where(m => !m.IsSpecialName) // exclude property accessors, operators, event add/remove + .Cast(); + + var properties = baseType.GetProperties(flags) + .Cast(); + + return methods.Concat(properties).ToList(); + } + + private static List GetAllPublicInstanceMembers(Type wrapperType) + { + var flags = BindingFlags.Public | BindingFlags.Instance; + + var methods = wrapperType.GetMethods(flags) + .Where(m => !m.IsSpecialName) + .Cast(); + + var properties = wrapperType.GetProperties(flags) + .Cast(); + + return methods.Concat(properties).ToList(); + } + + private static bool HasMatchingMember(List wrapperMembers, MemberInfo baseMember) + { + if (baseMember is MethodInfo baseMethod) + { + var baseParams = baseMethod.GetParameters().Select(p => p.ParameterType.Name).ToArray(); + + return wrapperMembers.OfType().Any(wm => + wm.Name == baseMethod.Name && + wm.GetParameters().Select(p => p.ParameterType.Name).SequenceEqual(baseParams)); + } + + if (baseMember is PropertyInfo baseProperty) + { + return wrapperMembers.OfType().Any(wp => wp.Name == baseProperty.Name && wp.DeclaringType != baseProperty.DeclaringType); + } + + return false; + } + + private static string DescribeMember(MemberInfo member) + { + if (member is MethodInfo method) + { + var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name)); + return $"{method.Name}({parameters})"; + } + + if (member is PropertyInfo property) + { + return $"{property.PropertyType.Name} {property.Name} {{ get; }}"; + } + + return member.Name; + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/GateKeeperTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/GateKeeperTests.cs new file mode 100644 index 0000000..42f29a3 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/GateKeeperTests.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Locks; +using Xunit; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Locks +{ + public class GateKeeperTests + { + [Fact] + public void IsGateOpen_ShouldBeTrue_Initially() + { + using var gateKeeper = new GateKeeper(); + + Assert.True(gateKeeper.IsGateOpen); + Assert.Equal(0, gateKeeper.LockedGateableThreadLocks); + } + + [Fact] + public void GetOrAdd_ShouldThrowArgumentNullException_WhenKeyIsNull() + { + using var gateKeeper = new GateKeeper(); + + Assert.Throws(() => gateKeeper.GetOrAdd(null!)); + } + + [Fact] + public void GetOrAdd_ShouldReturnSameInstance_ForSameKey() + { + using var gateKeeper = new GateKeeper(); + + var lock1 = gateKeeper.GetOrAdd("key1"); + var lock2 = gateKeeper.GetOrAdd("key1"); + + Assert.Same(lock1, lock2); + } + + [Fact] + public void Remove_ShouldThrowArgumentNullException_WhenKeyIsNull() + { + using var gateKeeper = new GateKeeper(); + + Assert.Throws(() => gateKeeper.Remove(null!)); + } + + [Fact] + public void Remove_ShouldThrowKeyNotFoundException_WhenKeyDoesNotExist() + { + using var gateKeeper = new GateKeeper(); + + Assert.Throws(() => gateKeeper.Remove("missing-key")); + } + + [Fact] + public void Remove_ShouldSucceed_ForExistingKey() + { + using var gateKeeper = new GateKeeper(); + gateKeeper.GetOrAdd("key1"); + + var exception = Record.Exception(() => gateKeeper.Remove("key1")); + + Assert.Null(exception); + } + + [Fact] + public void OpenGate_ShouldThrowInvalidOperationException_WhenGateAlreadyOpen() + { + using var gateKeeper = new GateKeeper(); + + Assert.Throws(() => gateKeeper.OpenGate()); + } + + [Fact] + public async Task CloseGateAsync_ShouldCompleteImmediately_WhenNoLocksHeld() + { + using var gateKeeper = new GateKeeper(); + + await gateKeeper.CloseGateAsync(); + + Assert.False(gateKeeper.IsGateOpen); + } + + [Fact] + public async Task CloseGateAsync_ShouldWait_UntilAllGatedLocksAreUnlocked() + { + using var gateKeeper = new GateKeeper(); + var gatedLock = gateKeeper.GetOrAdd("key1"); + + await gatedLock.LockAsync(); + + var closeGateTask = gateKeeper.CloseGateAsync(); + + var completedEarly = await Task.WhenAny(closeGateTask, Task.Delay(200)) == closeGateTask; + Assert.False(completedEarly); + + gatedLock.Unlock(); + + var completed = await Task.WhenAny(closeGateTask, Task.Delay(1000)) == closeGateTask; + Assert.True(completed); + Assert.False(gateKeeper.IsGateOpen); + } + + [Fact] + public async Task OpenGate_ShouldAllowNewLocks_AfterGateWasClosed() + { + using var gateKeeper = new GateKeeper(); + var gatedLock = gateKeeper.GetOrAdd("key1"); + + await gateKeeper.CloseGateAsync(); + + var lockTask = gatedLock.LockAsync(); + var completedEarly = await Task.WhenAny(lockTask, Task.Delay(200)) == lockTask; + Assert.False(completedEarly); + + gateKeeper.OpenGate(); + + Assert.True(gateKeeper.IsGateOpen); + + var completed = await Task.WhenAny(lockTask, Task.Delay(1000)) == lockTask; + Assert.True(completed); + } + + [Fact] + public async Task CloseGateAsync_ShouldThrowOperationCanceledException_WhenCancelledWhileWaitingForLocks() + { + using var gateKeeper = new GateKeeper(); + var gatedLock = gateKeeper.GetOrAdd("key1"); + + await gatedLock.LockAsync(); + + using var cts = new CancellationTokenSource(); + var closeGateTask = gateKeeper.CloseGateAsync(cts.Token); + + cts.Cancel(); + + await Assert.ThrowsAsync(() => closeGateTask); + + gatedLock.Unlock(); + } + + [Fact] + public async Task CloseGateAsync_ShouldThrowObjectDisposedException_WhenAlreadyDisposed() + { + var gateKeeper = new GateKeeper(); + gateKeeper.Dispose(); + + await Assert.ThrowsAsync(() => gateKeeper.CloseGateAsync()); + } + + [Fact] + public void GetOrAdd_ShouldThrowObjectDisposedException_WhenAlreadyDisposed() + { + var gateKeeper = new GateKeeper(); + gateKeeper.Dispose(); + + Assert.Throws(() => gateKeeper.GetOrAdd("key1")); + } + + [Fact] + public void Dispose_ShouldBeIdempotent() + { + var gateKeeper = new GateKeeper(); + + gateKeeper.Dispose(); + var exception = Record.Exception(() => gateKeeper.Dispose()); + + Assert.Null(exception); + } + + [Fact] + public async Task Dispose_ShouldDisposeAllGatedLocks() + { + var gateKeeper = new GateKeeper(); + var gatedLock = gateKeeper.GetOrAdd("key1"); + + await gatedLock.LockAsync(); + gatedLock.Unlock(); + + gateKeeper.Dispose(); + + await Assert.ThrowsAsync(() => gatedLock.LockAsync()); + } + + [Fact] + public async Task MultipleGatedLocks_ShouldIndependentlyIncrementAndDecrement_LockedCount() + { + using var gateKeeper = new GateKeeper(); + + var lock1 = gateKeeper.GetOrAdd("key1"); + var lock2 = gateKeeper.GetOrAdd("key2"); + + await lock1.LockAsync(); + Assert.Equal(1, gateKeeper.LockedGateableThreadLocks); + + await lock2.LockAsync(); + Assert.Equal(2, gateKeeper.LockedGateableThreadLocks); + + lock1.Unlock(); + Assert.Equal(1, gateKeeper.LockedGateableThreadLocks); + + lock2.Unlock(); + Assert.Equal(0, gateKeeper.LockedGateableThreadLocks); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/GatedThreadLockTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/GatedThreadLockTests.cs new file mode 100644 index 0000000..c4d4aa2 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/GatedThreadLockTests.cs @@ -0,0 +1,117 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Locks; +using Xunit; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Locks +{ + public class GatedThreadLockTests + { + [Fact] + public async Task LockAsync_ShouldThrowInvalidOperationException_WhenNotLinkedToGateKeeper() + { + using var gatedLock = new GatedThreadLock(); + + await Assert.ThrowsAsync(() => gatedLock.LockAsync()); + } + + [Fact] + public async Task LockAsync_ShouldSucceed_WhenLinkedViaGateKeeper() + { + using var gateKeeper = new GateKeeper(); + + var gatedLock = gateKeeper.GetOrAdd("key1"); + + await gatedLock.LockAsync(); + + Assert.True(gatedLock.IsLocked); + Assert.Equal(1, gateKeeper.LockedGateableThreadLocks); + } + + [Fact] + public async Task Unlock_ShouldReleaseLockAndExitGate() + { + using var gateKeeper = new GateKeeper(); + + var gatedLock = gateKeeper.GetOrAdd("key1"); + + await gatedLock.LockAsync(); + gatedLock.Unlock(); + + Assert.False(gatedLock.IsLocked); + Assert.Equal(0, gateKeeper.LockedGateableThreadLocks); + } + + [Fact] + public async Task LockAsync_ShouldBlockSecondCaller_ForSameGatedLock_UntilUnlocked() + { + using var gateKeeper = new GateKeeper(); + + var gatedLock = gateKeeper.GetOrAdd("key1"); + + await gatedLock.LockAsync(); + + var secondLockTask = gatedLock.LockAsync(); + + var completedEarly = await Task.WhenAny(secondLockTask, Task.Delay(200)) == secondLockTask; + Assert.False(completedEarly); + + gatedLock.Unlock(); + + var completed = await Task.WhenAny(secondLockTask, Task.Delay(1000)) == secondLockTask; + Assert.True(completed); + } + + [Fact] + public async Task LockAsync_ShouldThrowOperationCanceledException_WhenCancelled() + { + using var gateKeeper = new GateKeeper(); + var gatedLock = gateKeeper.GetOrAdd("key1"); + + await gatedLock.LockAsync(); + + using var cts = new CancellationTokenSource(); + var waitingTask = gatedLock.LockAsync(cts.Token); + + cts.Cancel(); + + await Assert.ThrowsAsync(() => waitingTask); + } + + [Fact] + public async Task LockAsync_ShouldThrowObjectDisposedException_WhenLockDisposedWhileWaiting() + { + using var gateKeeper = new GateKeeper(); + var gatedLock = gateKeeper.GetOrAdd("key1"); + + await gatedLock.LockAsync(); + + var waitingTask = gatedLock.LockAsync(); + + gatedLock.Dispose(); + + await Assert.ThrowsAsync(() => waitingTask); + } + + [Fact] + public async Task LockAsync_ShouldBeBlocked_WhenGateIsClosed() + { + using var gateKeeper = new GateKeeper(); + var gatedLock = gateKeeper.GetOrAdd("key1"); + + // Gate starts open, close it (no locks currently held so this returns immediately) + await gateKeeper.CloseGateAsync(); + + var lockTask = gatedLock.LockAsync(); + + var completedEarly = await Task.WhenAny(lockTask, Task.Delay(200)) == lockTask; + Assert.False(completedEarly); + + gateKeeper.OpenGate(); + + var completed = await Task.WhenAny(lockTask, Task.Delay(1000)) == lockTask; + Assert.True(completed); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MethodThreadLockTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MethodThreadLockTests.cs new file mode 100644 index 0000000..b1aed6c --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MethodThreadLockTests.cs @@ -0,0 +1,168 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Locks; +using Xunit; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Locks +{ + public class MethodThreadLockTests + { + private async Task MethodA(MethodThreadLock methodLock, int delayMs) + { + await methodLock.LockAsync(); + try + { + await Task.Delay(delayMs); + } + finally + { + methodLock.Unlock(); + } + } + + private async Task MethodB(MethodThreadLock methodLock, int delayMs) + { + await methodLock.LockAsync(); + try + { + await Task.Delay(delayMs); + } + finally + { + methodLock.Unlock(); + } + } + + [Fact] + public async Task LockAsync_ShouldUseCallerMemberName_ToScopeLock() + { + using var methodLock = new MethodThreadLock(); + + var firstCall = MethodA(methodLock, 200); + + await Task.Delay(20); // allow first call to acquire lock + + var secondCall = MethodA(methodLock, 20); + + var completedEarly = await Task.WhenAny(secondCall, Task.Delay(50)) == secondCall; + Assert.False(completedEarly); + + await Task.WhenAll(firstCall, secondCall); + } + + [Fact] + public async Task LockAsync_ShouldNotBlock_DifferentMethods() + { + using var methodLock = new MethodThreadLock(); + + var callA = MethodA(methodLock, 200); + var callB = MethodB(methodLock, 200); + + var completed = await Task.WhenAny(Task.WhenAll(callA, callB), Task.Delay(500)); + + Assert.Equal(Task.WhenAll(callA, callB).IsCompletedSuccessfully, completed.IsCompletedSuccessfully); + } + + [Fact] + public async Task LockAsync_WithExplicitMethodName_ShouldScopeIndependently() + { + using var methodLock = new MethodThreadLock(); + + await methodLock.LockAsync("CustomKey1"); + + var otherKeyTask = methodLock.LockAsync("CustomKey2"); + var completed = await Task.WhenAny(otherKeyTask, Task.Delay(500)) == otherKeyTask; + + Assert.True(completed); + + methodLock.Unlock("CustomKey1"); + methodLock.Unlock("CustomKey2"); + } + + [Fact] + public async Task LockAsync_ShouldThrowArgumentNullException_WhenMethodNameIsNullOrWhiteSpace() + { + using var methodLock = new MethodThreadLock(); + + await Assert.ThrowsAsync(() => methodLock.LockAsync(" ")); + } + + [Fact] + public void Unlock_ShouldThrowArgumentNullException_WhenMethodNameIsNullOrWhiteSpace() + { + using var methodLock = new MethodThreadLock(); + + Assert.Throws(() => methodLock.Unlock(" ")); + } + + [Fact] + public async Task LockAsync_ShouldThrowOperationCanceledException_WhenCancelled() + { + using var methodLock = new MethodThreadLock(); + + await methodLock.LockAsync(nameof(LockAsync_ShouldThrowOperationCanceledException_WhenCancelled)); + + using var cts = new CancellationTokenSource(); + var waitingTask = methodLock.LockAsync(nameof(LockAsync_ShouldThrowOperationCanceledException_WhenCancelled), cts.Token); + + cts.Cancel(); + + await Assert.ThrowsAsync(() => waitingTask); + } + + [Fact] + public async Task LockAsync_ShouldThrowObjectDisposedException_WhenDisposedWhileWaiting() + { + var methodLock = new MethodThreadLock(); + + await methodLock.LockAsync("SharedMethod"); + + var waitingTask = methodLock.LockAsync("SharedMethod"); + methodLock.Dispose(); + + await Assert.ThrowsAsync(() => waitingTask); + } + + [Fact] + public async Task LockAsync_ShouldOnlyAllowOneCaller_PerMethodName_UnderConcurrentAccess() + { + using var methodLock = new MethodThreadLock(); + var counter = 0; + var maxObservedConcurrency = 0; + var gate = new object(); + + async Task Worker() + { + await methodLock.LockAsync(nameof(Worker)); + try + { + lock (gate) + { + counter++; + maxObservedConcurrency = Math.Max(maxObservedConcurrency, counter); + } + + await Task.Delay(20); + + lock (gate) + { + counter--; + } + } + finally + { + methodLock.Unlock(nameof(Worker)); + } + } + + var tasks = new Task[10]; + for (int i = 0; i < tasks.Length; i++) + tasks[i] = Worker(); + + await Task.WhenAll(tasks); + + Assert.Equal(1, maxObservedConcurrency); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MultipleThreadLockManagerTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MultipleThreadLockManagerTests.cs new file mode 100644 index 0000000..c46c8b4 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/MultipleThreadLockManagerTests.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Locks; +using Xunit; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Locks +{ + public class MultipleThreadLockManagerTests + { + [Fact] + public async Task LockAsync_ShouldThrowArgumentNullException_WhenKeyIsNull() + { + using var manager = new MultipleThreadLockManager(); + + await Assert.ThrowsAsync(() => manager.LockAsync(null!)); + } + + [Fact] + public void Unlock_ShouldThrowArgumentNullException_WhenKeyIsNull() + { + using var manager = new MultipleThreadLockManager(); + + Assert.Throws(() => manager.Unlock(null!)); + } + + [Fact] + public void Unlock_ShouldThrowKeyNotFoundException_WhenKeyDoesNotExist() + { + using var manager = new MultipleThreadLockManager(); + + Assert.Throws(() => manager.Unlock("missing-key")); + } + + [Fact] + public async Task Unlock_ShouldThrowSynchronizationLockException_WhenKeyLockIsNotHeld() + { + using var manager = new MultipleThreadLockManager(); + + await manager.LockAsync("key1"); + manager.Unlock("key1"); + + Assert.Throws(() => manager.Unlock("key1")); + } + + [Fact] + public async Task LockAsync_ShouldCreateSeparateLocks_ForDifferentKeys() + { + using var manager = new MultipleThreadLockManager(); + + var lockTask1 = manager.LockAsync("key1"); + var lockTask2 = manager.LockAsync("key2"); + + var waitTask = Task.Delay(1000); + var completed = await Task.WhenAny(Task.WhenAll(lockTask1, lockTask2), waitTask); + + Assert.NotEqual(waitTask, completed); + } + + [Fact] + public async Task LockAsync_ShouldBlock_ForSameKey_UntilUnlocked() + { + using var manager = new MultipleThreadLockManager(); + + await manager.LockAsync("shared-key"); + + var secondLockTask = manager.LockAsync("shared-key"); + + var completedEarly = await Task.WhenAny(secondLockTask, Task.Delay(200)) == secondLockTask; + Assert.False(completedEarly); + + manager.Unlock("shared-key"); + + var completed = await Task.WhenAny(secondLockTask, Task.Delay(1000)) == secondLockTask; + Assert.True(completed); + } + + [Fact] + public async Task LockAsync_ShouldThrowOperationCanceledException_WhenCancelled() + { + using var manager = new MultipleThreadLockManager(); + + await manager.LockAsync("key1"); + + using var cts = new CancellationTokenSource(); + var waitingTask = manager.LockAsync("key1", cts.Token); + + cts.Cancel(); + + await Assert.ThrowsAsync(() => waitingTask); + } + + [Fact] + public async Task LockAsync_ShouldThrowObjectDisposedException_WhenManagerIsDisposedWhileWaiting() + { + var manager = new MultipleThreadLockManager(); + + await manager.LockAsync("key1"); + + var waitingTask = manager.LockAsync("key1"); + + manager.Dispose(); + + await Assert.ThrowsAsync(() => waitingTask); + } + + [Fact] + public async Task LockAsync_ShouldThrowObjectDisposedException_WhenAlreadyDisposed() + { + var manager = new MultipleThreadLockManager(); + manager.Dispose(); + + await Assert.ThrowsAsync(() => manager.LockAsync("key1")); + } + + [Fact] + public void Unlock_ShouldThrowObjectDisposedException_WhenAlreadyDisposed() + { + var manager = new MultipleThreadLockManager(); + manager.Dispose(); + + Assert.Throws(() => manager.Unlock("key1")); + } + + [Fact] + public void Dispose_ShouldBeIdempotent() + { + var manager = new MultipleThreadLockManager(); + + manager.Dispose(); + var exception = Record.Exception(() => manager.Dispose()); + + Assert.Null(exception); + } + + [Fact] + public async Task LockAsync_ShouldOnlyAllowOneThreadPerKey_UnderConcurrentAccess() + { + using var manager = new MultipleThreadLockManager(); + var counter = 0; + var maxObservedConcurrency = 0; + var gate = new object(); + + async Task Worker() + { + await manager.LockAsync("shared-key"); + try + { + lock (gate) + { + counter++; + maxObservedConcurrency = Math.Max(maxObservedConcurrency, counter); + } + + await Task.Delay(20); + + lock (gate) + { + counter--; + } + } + finally + { + manager.Unlock("shared-key"); + } + } + + var tasks = new Task[10]; + for (int i = 0; i < tasks.Length; i++) + tasks[i] = Worker(); + + await Task.WhenAll(tasks); + + Assert.Equal(1, maxObservedConcurrency); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/SingleThreadLockTests.cs b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/SingleThreadLockTests.cs new file mode 100644 index 0000000..bfe07ea --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/Locks/SingleThreadLockTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Locks; +using Xunit; + +namespace ThunderDesign.Net_PCL.Threading.UnitTests.Locks +{ + public class SingleThreadLockTests + { + [Fact] + public void IsLocked_ShouldBeFalse_WhenNotLocked() + { + using var threadLock = new SingleThreadLock(); + + Assert.False(threadLock.IsLocked); + } + + [Fact] + public async Task LockAsync_ShouldSetIsLocked_WhenLockAcquired() + { + using var threadLock = new SingleThreadLock(); + + await threadLock.LockAsync(); + + Assert.True(threadLock.IsLocked); + } + + [Fact] + public async Task Unlock_ShouldReleaseLock_AllowingSubsequentLock() + { + using var threadLock = new SingleThreadLock(); + + await threadLock.LockAsync(); + threadLock.Unlock(); + + Assert.False(threadLock.IsLocked); + + // Should be able to lock again without blocking + var task = threadLock.LockAsync(); + var completed = await Task.WhenAny(task, Task.Delay(1000)) == task; + + Assert.True(completed); + Assert.True(threadLock.IsLocked); + } + + [Fact] + public void Unlock_ShouldThrow_WhenNotCurrentlyLocked() + { + using var threadLock = new SingleThreadLock(); + + Assert.Throws(() => threadLock.Unlock()); + } + + [Fact] + public async Task SecondLockAsync_ShouldWait_UntilFirstIsUnlocked() + { + using var threadLock = new SingleThreadLock(); + + await threadLock.LockAsync(); + + var secondLockTask = threadLock.LockAsync(); + + // The second lock should not complete while the first still holds the lock + var completedEarly = await Task.WhenAny(secondLockTask, Task.Delay(200)) == secondLockTask; + Assert.False(completedEarly); + + threadLock.Unlock(); + + // Now the second lock should complete + var completed = await Task.WhenAny(secondLockTask, Task.Delay(1000)) == secondLockTask; + Assert.True(completed); + Assert.True(threadLock.IsLocked); + } + + [Fact] + public async Task LockAsync_ShouldThrowOperationCanceledException_WhenCancellationTokenIsCancelled() + { + using var threadLock = new SingleThreadLock(); + + await threadLock.LockAsync(); + + using var cts = new CancellationTokenSource(); + var waitingTask = threadLock.LockAsync(cts.Token); + + cts.Cancel(); + + await Assert.ThrowsAsync(() => waitingTask); + } + + [Fact] + public async Task LockAsync_ShouldThrowObjectDisposedException_WhenLockIsDisposedWhileWaiting() + { + var threadLock = new SingleThreadLock(); + + await threadLock.LockAsync(); + + var waitingTask = threadLock.LockAsync(); + + threadLock.Dispose(); + + await Assert.ThrowsAsync(() => waitingTask); + } + + [Fact] + public async Task LockAsync_ShouldThrowObjectDisposedException_WhenAlreadyDisposed() + { + var threadLock = new SingleThreadLock(); + threadLock.Dispose(); + + await Assert.ThrowsAsync(() => threadLock.LockAsync()); + } + + [Fact] + public void Unlock_ShouldThrowObjectDisposedException_WhenAlreadyDisposed() + { + var threadLock = new SingleThreadLock(); + threadLock.Dispose(); + + Assert.Throws(() => threadLock.Unlock()); + } + + [Fact] + public void Dispose_ShouldBeIdempotent() + { + var threadLock = new SingleThreadLock(); + + threadLock.Dispose(); + var exception = Record.Exception(() => threadLock.Dispose()); + + Assert.Null(exception); + } + + [Fact] + public async Task LockAsync_ShouldOnlyAllowOneThread_UnderConcurrentAccess() + { + using var threadLock = new SingleThreadLock(); + var counter = 0; + var maxObservedConcurrency = 0; + var gate = new object(); + + async Task Worker() + { + await threadLock.LockAsync(); + try + { + lock (gate) + { + counter++; + maxObservedConcurrency = Math.Max(maxObservedConcurrency, counter); + } + + await Task.Delay(20); + + lock (gate) + { + counter--; + } + } + finally + { + threadLock.Unlock(); + } + } + + var tasks = new Task[10]; + for (int i = 0; i < tasks.Length; i++) + tasks[i] = Worker(); + + await Task.WhenAll(tasks); + + Assert.Equal(1, maxObservedConcurrency); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading.UnitTests/ThunderDesign.Net-PCL.Threading.UnitTests.csproj b/src/ThunderDesign.Net-PCL.Threading.UnitTests/ThunderDesign.Net-PCL.Threading.UnitTests.csproj new file mode 100644 index 0000000..c8807fc --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading.UnitTests/ThunderDesign.Net-PCL.Threading.UnitTests.csproj @@ -0,0 +1,27 @@ + + + + net8.0;net9.0;net10.0 + ThunderDesign.Net_PCL.Threading.UnitTests + enable + enable + false + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ThunderDesign.Net-PCL.Threading.nuspec.in b/src/ThunderDesign.Net-PCL.Threading.nuspec.in index 2bad58c..ea98328 100644 --- a/src/ThunderDesign.Net-PCL.Threading.nuspec.in +++ b/src/ThunderDesign.Net-PCL.Threading.nuspec.in @@ -18,9 +18,9 @@ - + @@ -28,16 +28,16 @@ - + - + diff --git a/src/ThunderDesign.Net-PCL.Threading.sln b/src/ThunderDesign.Net-PCL.Threading.sln index cc9b7c4..2c57cdf 100644 --- a/src/ThunderDesign.Net-PCL.Threading.sln +++ b/src/ThunderDesign.Net-PCL.Threading.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32127.271 +# Visual Studio Version 18 +VisualStudioVersion = 18.7.11925.98 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThunderDesign.Net-PCL.Threading", "ThunderDesign.Net-PCL.Threading\ThunderDesign.Net-PCL.Threading.csproj", "{522FA62F-E5B1-4C52-ABB0-F87BF75F1A32}" EndProject @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThunderDesign.Net-PCL.Sourc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThunderDesign.Net-PCL.Threading.Shared", "ThunderDesign.Net-PCL.Threading.Shared\ThunderDesign.Net-PCL.Threading.Shared.csproj", "{66B44DB3-424C-45F2-876F-BC43D1D07A85}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThunderDesign.Net-PCL.Threading.UnitTests", "ThunderDesign.Net-PCL.Threading.UnitTests\ThunderDesign.Net-PCL.Threading.UnitTests.csproj", "{7138EDED-69D8-4561-A55C-A9915EDE7CAF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {66B44DB3-424C-45F2-876F-BC43D1D07A85}.Debug|Any CPU.Build.0 = Debug|Any CPU {66B44DB3-424C-45F2-876F-BC43D1D07A85}.Release|Any CPU.ActiveCfg = Release|Any CPU {66B44DB3-424C-45F2-876F-BC43D1D07A85}.Release|Any CPU.Build.0 = Release|Any CPU + {7138EDED-69D8-4561-A55C-A9915EDE7CAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7138EDED-69D8-4561-A55C-A9915EDE7CAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7138EDED-69D8-4561-A55C-A9915EDE7CAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7138EDED-69D8-4561-A55C-A9915EDE7CAF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/CollectionThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/CollectionThreadSafe.cs index 1ed9143..2faecc2 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/CollectionThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/CollectionThreadSafe.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading; using ThunderDesign.Net.Threading.Interfaces; @@ -19,6 +20,11 @@ public bool IsSynchronized get { return true; } } + bool ICollection.IsSynchronized + { + get { return true; } + } + public new T this[int index] { get @@ -194,7 +200,7 @@ public T GetItemByIndex(int index) _ReaderWriterLockSlim.ExitReadLock(); } } - #endregion +#endregion #region variables protected readonly ReaderWriterLockSlim _ReaderWriterLockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/DictionaryThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/DictionaryThreadSafe.cs index 2dc8ba6..d3f6d23 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/DictionaryThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/DictionaryThreadSafe.cs @@ -1,6 +1,11 @@ using System; +using System.Collections; using System.Collections.Generic; +#if NET8_0_OR_GREATER using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +#endif #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER using System.Runtime.Serialization; @@ -10,7 +15,7 @@ namespace ThunderDesign.Net.Threading.Collections { - public class DictionaryThreadSafe : Dictionary, IDictionaryThreadSafe + public class DictionaryThreadSafe : Dictionary, IDictionaryThreadSafe where TKey : notnull { #region constructors public DictionaryThreadSafe() : base() { } @@ -32,6 +37,11 @@ public bool IsSynchronized get { return true; } } + bool ICollection.IsSynchronized + { + get { return true; } + } + public new IEqualityComparer Comparer { get @@ -64,6 +74,24 @@ public bool IsSynchronized } } +#if NET9_0_OR_GREATER + public new int Capacity + { + get + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.Capacity; + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } + } +#endif + public new KeyCollection Keys { get @@ -165,25 +193,25 @@ public bool IsSynchronized } } - public new bool Remove(TKey key) + public new bool ContainsValue(TValue value) { - _ReaderWriterLockSlim.EnterWriteLock(); + _ReaderWriterLockSlim.EnterReadLock(); try { - return base.Remove(key); + return base.ContainsValue(value); } finally { - _ReaderWriterLockSlim.ExitWriteLock(); + _ReaderWriterLockSlim.ExitReadLock(); } } - public new bool TryGetValue(TKey key, out TValue value) + public new Enumerator GetEnumerator() { _ReaderWriterLockSlim.EnterReadLock(); try { - return base.TryGetValue(key, out value); + return base.GetEnumerator(); } finally { @@ -191,46 +219,48 @@ public bool IsSynchronized } } - public new bool ContainsValue(TValue value) +#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER + +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] +#endif + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) { _ReaderWriterLockSlim.EnterReadLock(); try { - return base.ContainsValue(value); + base.GetObjectData(info, context); } finally { _ReaderWriterLockSlim.ExitReadLock(); } } +#endif - public new Enumerator GetEnumerator() +#if NET9_0_OR_GREATER + public new AlternateLookup GetAlternateLookup() + where TAlternateKey : notnull, allows ref struct { _ReaderWriterLockSlim.EnterReadLock(); try { - return base.GetEnumerator(); + return base.GetAlternateLookup(); } finally { _ReaderWriterLockSlim.ExitReadLock(); } } - -#if NET8_0_OR_GREATER - [Obsolete("GetObjectData is obsolete in .Net 8", false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - } -#elif NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER - [System.Security.SecurityCritical] - public override void GetObjectData(SerializationInfo info, StreamingContext context) + public new bool TryGetAlternateLookup( + out AlternateLookup lookup) + where TAlternateKey : notnull, allows ref struct { _ReaderWriterLockSlim.EnterReadLock(); try { - base.GetObjectData(info, context); + return base.TryGetAlternateLookup(out lookup); } finally { @@ -239,8 +269,26 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } #endif + public TValue GetOrAdd(TKey key, Func valueFactory) + { + _ReaderWriterLockSlim.EnterUpgradeableReadLock(); + try + { + if (!base.TryGetValue(key, out TValue? value)) + { + value = valueFactory(key); + Add(key, value); + } + return value; + } + finally + { + _ReaderWriterLockSlim.ExitUpgradeableReadLock(); + } + } + #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER - public override void OnDeserialization(object sender) + public override void OnDeserialization(object? sender) { _ReaderWriterLockSlim.EnterReadLock(); try @@ -254,13 +302,12 @@ public override void OnDeserialization(object sender) } #endif -#if NET6_0_OR_GREATER - public new bool TryAdd(TKey key, TValue value) + public new bool Remove(TKey key) { _ReaderWriterLockSlim.EnterWriteLock(); try { - return base.TryAdd(key, value); + return base.Remove(key); } finally { @@ -268,7 +315,8 @@ public override void OnDeserialization(object sender) } } - public new bool Remove(TKey key, out TValue value) +#if NET6_0_OR_GREATER + public new bool Remove(TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TValue value) { _ReaderWriterLockSlim.EnterWriteLock(); try @@ -280,13 +328,32 @@ public override void OnDeserialization(object sender) _ReaderWriterLockSlim.ExitWriteLock(); } } +#endif + +#if NET6_0_OR_GREATER + public new bool TryGetValue(TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TValue value) +#else + public new bool TryGetValue(TKey key, out TValue value) +#endif + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.TryGetValue(key, out value); + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } - public new int EnsureCapacity(int capacity) +#if NET6_0_OR_GREATER + public new bool TryAdd(TKey key, TValue value) { _ReaderWriterLockSlim.EnterWriteLock(); try { - return base.EnsureCapacity(capacity); + return base.TryAdd(key, value); } finally { @@ -294,12 +361,12 @@ public override void OnDeserialization(object sender) } } - public new void TrimExcess() + public new int EnsureCapacity(int capacity) { _ReaderWriterLockSlim.EnterWriteLock(); try { - base.TrimExcess(); + return base.EnsureCapacity(capacity); } finally { @@ -307,6 +374,8 @@ public override void OnDeserialization(object sender) } } + public new void TrimExcess() => TrimExcess(Count); + public new void TrimExcess(int capacity) { _ReaderWriterLockSlim.EnterWriteLock(); @@ -320,7 +389,7 @@ public override void OnDeserialization(object sender) } } #endif - #endregion +#endregion #region variables protected readonly ReaderWriterLockSlim _ReaderWriterLockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/HashSetThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/HashSetThreadSafe.cs index dc59472..7335322 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/HashSetThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/HashSetThreadSafe.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Threading; using ThunderDesign.Net.Threading.Interfaces; @@ -55,7 +57,25 @@ public bool IsSynchronized } } } - #endregion + +#if NET9_0_OR_GREATER + public new int Capacity + { + get + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.Capacity; + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } + } +#endif + #endregion #region methods public new bool Add(T item) @@ -293,7 +313,7 @@ public bool IsSynchronized } #if NET8_0_OR_GREATER - public new bool TryGetValue(T equalValue, out T actualValue) + public new bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue) { _ReaderWriterLockSlim.EnterReadLock(); try @@ -307,6 +327,78 @@ public bool IsSynchronized } #endif +#if NET6_0_OR_GREATER + public new int EnsureCapacity(int capacity) + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + return base.EnsureCapacity(capacity); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } + + public new void TrimExcess() + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + base.TrimExcess(); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } +#endif + +#if NET9_0_OR_GREATER + public new void TrimExcess(int capacity) + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + base.TrimExcess(capacity); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } + + public new AlternateLookup GetAlternateLookup() + where TAlternateKey : notnull, allows ref struct + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.GetAlternateLookup(); + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } + + public new bool TryGetAlternateLookup( + out AlternateLookup lookup) + where TAlternateKey : notnull, allows ref struct + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.TryGetAlternateLookup(out lookup); + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } +#endif + public new IEnumerator GetEnumerator() { _ReaderWriterLockSlim.EnterReadLock(); @@ -319,7 +411,41 @@ public bool IsSynchronized _ReaderWriterLockSlim.ExitReadLock(); } } - #endregion + +#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER + +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] +#endif + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + base.GetObjectData(info, context); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } + + public override void OnDeserialization(object? sender) + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + base.OnDeserialization(sender); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } +#endif + +#endregion #region variables protected readonly ReaderWriterLockSlim _ReaderWriterLockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/LinkedListThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/LinkedListThreadSafe.cs index 3966780..322be25 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/LinkedListThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/LinkedListThreadSafe.cs @@ -1,4 +1,7 @@ +using System; +using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Threading; using ThunderDesign.Net.Threading.Interfaces; @@ -19,6 +22,13 @@ public bool IsSynchronized get { return true; } } +#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER + bool ICollection.IsSynchronized + { + get { return true; } + } +#endif + public new int Count { get @@ -35,7 +45,7 @@ public bool IsSynchronized } } - public new LinkedListNode First + public new LinkedListNode? First { get { @@ -51,7 +61,7 @@ public bool IsSynchronized } } - public new LinkedListNode Last + public new LinkedListNode? Last { get { @@ -212,7 +222,7 @@ public bool IsSynchronized } } - public new LinkedListNode Find(T value) + public new LinkedListNode? Find(T value) { _ReaderWriterLockSlim.EnterReadLock(); try @@ -225,7 +235,7 @@ public bool IsSynchronized } } - public new LinkedListNode FindLast(T value) + public new LinkedListNode? FindLast(T value) { _ReaderWriterLockSlim.EnterReadLock(); try @@ -302,6 +312,40 @@ public bool IsSynchronized _ReaderWriterLockSlim.ExitWriteLock(); } } + +#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER + +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] +#endif + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + base.GetObjectData(info, context); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } + + public override void OnDeserialization(object? sender) + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + base.OnDeserialization(sender); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } +#endif + #endregion #region variables diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/ListThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/ListThreadSafe.cs index a79f221..1301158 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/ListThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/ListThreadSafe.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading; @@ -22,6 +23,11 @@ public bool IsSynchronized get { return true; } } + bool ICollection.IsSynchronized + { + get { return true; } + } + public new T this[int index] { get @@ -268,7 +274,7 @@ public bool IsSynchronized } } - public new T Find(Predicate match) + public new T? Find(Predicate match) { _ReaderWriterLockSlim.EnterReadLock(); try @@ -333,7 +339,7 @@ public bool IsSynchronized } } - public new T FindLast(Predicate match) + public new T? FindLast(Predicate match) { _ReaderWriterLockSlim.EnterReadLock(); try @@ -608,6 +614,21 @@ public bool IsSynchronized } } +#if NET8_0_OR_GREATER + public new List Slice(int start, int length) + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.Slice(start, length); + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } +#endif + public new void Sort(Comparison comparison) { _ReaderWriterLockSlim.EnterWriteLock(); diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/ObservableCollectionThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/ObservableCollectionThreadSafe.cs index 442bfa3..f71f84b 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/ObservableCollectionThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/ObservableCollectionThreadSafe.cs @@ -17,29 +17,32 @@ public ObservableCollectionThreadSafe(bool waitOnNotifying = true) : base() _waitOnNotifyingRef = waitOnNotifying; } - public ObservableCollectionThreadSafe(List list, bool waitOnNotifying = true) - : base((list != null) ? new List(list.Count) : list) + public ObservableCollectionThreadSafe(IList list, bool waitOnNotifying = true) + : this(waitOnNotifying) { - _waitOnNotifyingRef = waitOnNotifying; + if (list == null) + throw new ArgumentNullException("list"); + // doesn't copy the list (contrary to the documentation) - it uses the // list directly as its storage. So we do the copying here. - // CopyFrom(list); } public ObservableCollectionThreadSafe(IEnumerable collection, bool waitOnNotifying = true) + : this(waitOnNotifying) { - _waitOnNotifyingRef = waitOnNotifying; if (collection == null) throw new ArgumentNullException("collection"); + // doesn't copy the list (contrary to the documentation) - it uses the + // list directly as its storage. So we do the copying here. CopyFrom(collection); } #endregion #region event handlers - public event NotifyCollectionChangedEventHandler CollectionChanged; - public event PropertyChangedEventHandler PropertyChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event PropertyChangedEventHandler? PropertyChanged; #endregion #region properties @@ -68,7 +71,7 @@ private void CopyFrom(IEnumerable collection) public void Move(int oldIndex, int newIndex) { - T removedItem = default; + T removedItem; var notifyAndWait = WaitOnNotifying; if (notifyAndWait) @@ -179,7 +182,7 @@ public void Reset() { var result = false; int index = -1; - T removedItem = default; + T? removedItem = default; var notifyAndWait = WaitOnNotifying; if (notifyAndWait) @@ -215,7 +218,7 @@ public void Reset() public new void RemoveAt(int index) { - T removedItem = default; + T? removedItem = default; var notifyAndWait = WaitOnNotifying; if (notifyAndWait) diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/ObservableDictionaryThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/ObservableDictionaryThreadSafe.cs index e27c79c..bd255d3 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/ObservableDictionaryThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/ObservableDictionaryThreadSafe.cs @@ -1,12 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using ThunderDesign.Net.Threading.Extentions; using ThunderDesign.Net.Threading.Interfaces; namespace ThunderDesign.Net.Threading.Collections { - public class ObservableDictionaryThreadSafe : DictionaryThreadSafe, IObservableDictionaryThreadSafe + public class ObservableDictionaryThreadSafe : DictionaryThreadSafe, IObservableDictionaryThreadSafe where TKey : notnull { #region constructors public ObservableDictionaryThreadSafe(bool waitOnNotifying = true) : base() @@ -41,8 +43,8 @@ public ObservableDictionaryThreadSafe(IDictionary dictionary, IEqu #endregion #region event handlers - public event NotifyCollectionChangedEventHandler CollectionChanged; - public event PropertyChangedEventHandler PropertyChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event PropertyChangedEventHandler? PropertyChanged; #endregion @@ -133,10 +135,20 @@ public bool WaitOnNotifying } } + public new TValue GetOrAdd(TKey key, Func valueFactory) + { + if (!TryGetValue(key, out TValue? value)) + { + value = valueFactory(key); + Add(key, value); + } + return value; + } + public new bool Remove(TKey key) { bool result = false; - TValue value; + TValue? value = default; var notifyAndWait = WaitOnNotifying; if (notifyAndWait) @@ -190,10 +202,11 @@ public void Reset() } #if NET6_0_OR_GREATER - public new bool TryAdd(TKey key, TValue value) + public new bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) { var notifyAndWait = WaitOnNotifying; bool result; + value = default; if (notifyAndWait) _ReaderWriterLockSlim.EnterUpgradeableReadLock(); try @@ -201,7 +214,7 @@ public void Reset() _ReaderWriterLockSlim.EnterWriteLock(); try { - result = base.TryAdd(key, value); + result = base.Remove(key, out value); } finally { @@ -212,7 +225,7 @@ public void Reset() OnPropertyChanged(nameof(Keys)); OnPropertyChanged(nameof(Values)); OnPropertyChanged(nameof(Count)); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, value)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, value)); } } finally @@ -223,11 +236,10 @@ public void Reset() return result; } - public new bool Remove(TKey key, out TValue value) + public new bool TryAdd(TKey key, TValue value) { var notifyAndWait = WaitOnNotifying; bool result; - value = default; if (notifyAndWait) _ReaderWriterLockSlim.EnterUpgradeableReadLock(); try @@ -235,7 +247,7 @@ public void Reset() _ReaderWriterLockSlim.EnterWriteLock(); try { - result = base.Remove(key, out value); + result = base.TryAdd(key, value); } finally { @@ -246,7 +258,7 @@ public void Reset() OnPropertyChanged(nameof(Keys)); OnPropertyChanged(nameof(Values)); OnPropertyChanged(nameof(Count)); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, value)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, value)); } } finally @@ -256,45 +268,6 @@ public void Reset() } return result; } - - public new int EnsureCapacity(int capacity) - { - _ReaderWriterLockSlim.EnterWriteLock(); - try - { - return base.EnsureCapacity(capacity); - } - finally - { - _ReaderWriterLockSlim.ExitWriteLock(); - } - } - - public new void TrimExcess() - { - _ReaderWriterLockSlim.EnterWriteLock(); - try - { - base.TrimExcess(); - } - finally - { - _ReaderWriterLockSlim.ExitWriteLock(); - } - } - - public new void TrimExcess(int capacity) - { - _ReaderWriterLockSlim.EnterWriteLock(); - try - { - base.TrimExcess(capacity); - } - finally - { - _ReaderWriterLockSlim.ExitWriteLock(); - } - } #endif public virtual void OnPropertyChanged(string propertyName) diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/QueueThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/QueueThreadSafe.cs index 33a6396..32d85d0 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/QueueThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/QueueThreadSafe.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using System.Threading; using ThunderDesign.Net.Threading.Interfaces; @@ -20,6 +21,11 @@ public bool IsSynchronized get { return true; } } + bool ICollection.IsSynchronized + { + get { return true; } + } + public new int Count { get @@ -35,6 +41,24 @@ public bool IsSynchronized } } } + +#if NET9_0_OR_GREATER + public new int Capacity + { + get + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.Capacity; + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } + } +#endif #endregion #region methods @@ -155,6 +179,21 @@ public bool IsSynchronized } } +#if NET9_0_OR_GREATER + public new void TrimExcess(int capacity) + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + base.TrimExcess(capacity); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } +#endif + #if NET6_0_OR_GREATER public new int EnsureCapacity(int capacity) { @@ -169,7 +208,7 @@ public bool IsSynchronized } } - public new bool TryDequeue(out T result) + public new bool TryDequeue([System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out T result) { _ReaderWriterLockSlim.EnterWriteLock(); try @@ -182,7 +221,7 @@ public bool IsSynchronized } } - public new bool TryPeek(out T result) + public new bool TryPeek([System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out T result) { _ReaderWriterLockSlim.EnterReadLock(); try diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/SortedDictionaryThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/SortedDictionaryThreadSafe.cs index 40a77e7..eb272b0 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/SortedDictionaryThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/SortedDictionaryThreadSafe.cs @@ -1,11 +1,13 @@ +using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using ThunderDesign.Net.Threading.Interfaces; namespace ThunderDesign.Net.Threading.Collections { #if NETSTANDARD1_3_OR_GREATER || NET6_0_OR_GREATER - public class SortedDictionaryThreadSafe : SortedDictionary, ISortedDictionaryThreadSafe + public class SortedDictionaryThreadSafe : SortedDictionary, ISortedDictionaryThreadSafe where TKey : notnull { #region constructors public SortedDictionaryThreadSafe() : base() { } @@ -23,6 +25,13 @@ public bool IsSynchronized get { return true; } } +#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER + bool ICollection.IsSynchronized + { + get { return true; } + } +#endif + public new IComparer Comparer { get @@ -169,6 +178,19 @@ public bool IsSynchronized } } + public new void CopyTo(KeyValuePair[] array, int index) + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + base.CopyTo(array, index); + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } + public new IEnumerator> GetEnumerator() { _ReaderWriterLockSlim.EnterReadLock(); @@ -194,8 +216,11 @@ public bool IsSynchronized _ReaderWriterLockSlim.ExitWriteLock(); } } - +#if NET6_0_OR_GREATER + public new bool TryGetValue(TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TValue value) +#else public new bool TryGetValue(TKey key, out TValue value) +#endif { _ReaderWriterLockSlim.EnterReadLock(); try @@ -207,7 +232,7 @@ public bool IsSynchronized _ReaderWriterLockSlim.ExitReadLock(); } } - #endregion +#endregion #region variables protected readonly ReaderWriterLockSlim _ReaderWriterLockSlim = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/SortedListThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/SortedListThreadSafe.cs index eb51e82..ce29442 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/SortedListThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/SortedListThreadSafe.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using System.Threading; using ThunderDesign.Net.Threading.Interfaces; namespace ThunderDesign.Net.Threading.Collections { #if NETSTANDARD1_3_OR_GREATER || NET6_0_OR_GREATER - public class SortedListThreadSafe : SortedList, ISortedListThreadSafe + public class SortedListThreadSafe : SortedList, ISortedListThreadSafe where TKey : notnull { #region constructors public SortedListThreadSafe() : base() { } @@ -27,6 +28,11 @@ public bool IsSynchronized get { return true; } } + bool ICollection.IsSynchronized + { + get { return true; } + } + public new int Capacity { get @@ -214,6 +220,34 @@ public bool IsSynchronized } } +#if NET8_0_OR_GREATER + public new TKey GetKeyAtIndex(int index) + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.GetKeyAtIndex(index); + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } + + public new TValue GetValueAtIndex(int index) + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.GetValueAtIndex(index); + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } +#endif + public new int IndexOfKey(TKey key) { _ReaderWriterLockSlim.EnterReadLock(); @@ -266,6 +300,21 @@ public bool IsSynchronized } } +#if NET8_0_OR_GREATER + public new void SetValueAtIndex(int index, TValue value) + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + base.SetValueAtIndex(index, value); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } +#endif + public new void TrimExcess() { _ReaderWriterLockSlim.EnterWriteLock(); @@ -279,7 +328,11 @@ public bool IsSynchronized } } +#if NET6_0_OR_GREATER + public new bool TryGetValue(TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TValue value) +#else public new bool TryGetValue(TKey key, out TValue value) +#endif { _ReaderWriterLockSlim.EnterReadLock(); try diff --git a/src/ThunderDesign.Net-PCL.Threading/Collections/StackThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Collections/StackThreadSafe.cs index 2c77319..5629365 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Collections/StackThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Collections/StackThreadSafe.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Collections.Generic; using System.Threading; using ThunderDesign.Net.Threading.Interfaces; @@ -21,6 +22,11 @@ public bool IsSynchronized get { return true; } } + bool ICollection.IsSynchronized + { + get { return true; } + } + public new int Count { get @@ -36,6 +42,24 @@ public bool IsSynchronized } } } + +#if NET9_0_OR_GREATER + public new int Capacity + { + get + { + _ReaderWriterLockSlim.EnterReadLock(); + try + { + return base.Capacity; + } + finally + { + _ReaderWriterLockSlim.ExitReadLock(); + } + } + } +#endif #endregion #region methods @@ -156,8 +180,36 @@ public bool IsSynchronized } } +#if NET9_0_OR_GREATER + public new void TrimExcess(int capacity) + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + base.TrimExcess(capacity); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } +#endif + #if NET6_0_OR_GREATER - public new bool TryPeek(out T result) + public new int EnsureCapacity(int capacity) + { + _ReaderWriterLockSlim.EnterWriteLock(); + try + { + return base.EnsureCapacity(capacity); + } + finally + { + _ReaderWriterLockSlim.ExitWriteLock(); + } + } + + public new bool TryPeek([System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out T result) { _ReaderWriterLockSlim.EnterReadLock(); try @@ -170,7 +222,7 @@ public bool IsSynchronized } } - public new bool TryPop(out T result) + public new bool TryPop([System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out T result) { _ReaderWriterLockSlim.EnterWriteLock(); try diff --git a/src/ThunderDesign.Net-PCL.Threading/DataCollections/ObservableDataDictionary.cs b/src/ThunderDesign.Net-PCL.Threading/DataCollections/ObservableDataDictionary.cs index 2fe4b0a..757d6d8 100644 --- a/src/ThunderDesign.Net-PCL.Threading/DataCollections/ObservableDataDictionary.cs +++ b/src/ThunderDesign.Net-PCL.Threading/DataCollections/ObservableDataDictionary.cs @@ -5,7 +5,7 @@ namespace ThunderDesign.Net.Threading.DataCollections { - public class ObservableDataDictionary : ObservableDictionaryThreadSafe, IObservableDataDictionary where TValue : IBindableDataObject + public class ObservableDataDictionary : ObservableDictionaryThreadSafe, IObservableDataDictionary where TKey : notnull where TValue : IBindableDataObject { #region constructors public ObservableDataDictionary(bool waitOnNotifying = true) : base(waitOnNotifying) { } diff --git a/src/ThunderDesign.Net-PCL.Threading/DataObjects/BindableDataObject.cs b/src/ThunderDesign.Net-PCL.Threading/DataObjects/BindableDataObject.cs index d2f91f9..c0ab0f4 100644 --- a/src/ThunderDesign.Net-PCL.Threading/DataObjects/BindableDataObject.cs +++ b/src/ThunderDesign.Net-PCL.Threading/DataObjects/BindableDataObject.cs @@ -5,7 +5,7 @@ namespace ThunderDesign.Net.Threading.DataObjects { - public class BindableDataObject : DataObject, IBindableDataObject + public class BindableDataObject : DataObject, IBindableDataObject where TKey : notnull { #region constructors public BindableDataObject(bool waitOnNotifying = true) : base() @@ -15,7 +15,7 @@ public BindableDataObject(bool waitOnNotifying = true) : base() #endregion #region event handlers - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; #endregion #region properties @@ -27,7 +27,7 @@ public bool WaitOnNotifying #endregion #region methods - protected override void SetId(Key value) + protected override void SetId(TKey value) { this.SetProperty(ref _idRef, value, _Locker, true, nameof(Id)); } diff --git a/src/ThunderDesign.Net-PCL.Threading/DataObjects/DataObject.cs b/src/ThunderDesign.Net-PCL.Threading/DataObjects/DataObject.cs index ddfbad2..b410708 100644 --- a/src/ThunderDesign.Net-PCL.Threading/DataObjects/DataObject.cs +++ b/src/ThunderDesign.Net-PCL.Threading/DataObjects/DataObject.cs @@ -4,10 +4,10 @@ namespace ThunderDesign.Net.Threading.DataObjects { - public class DataObject : ThreadObject, IDataObject + public class DataObject : ThreadObject, IDataObject where TKey : notnull { #region properties - public Key Id + public TKey Id { get { return GetId(); } set { SetId(value); } @@ -16,24 +16,24 @@ public Key Id object IDataObject.Id { get { return GetId(); } - set { SetId((Key)value); } + set { SetId((TKey)value); } } #endregion #region methods - protected virtual Key GetId() + protected virtual TKey GetId() { return this.GetProperty(ref _idRef, _Locker); } - protected virtual void SetId(Key value) + protected virtual void SetId(TKey value) { this.SetProperty(ref _idRef, value, _Locker); } #endregion #region variables - protected Key _idRef = default; + protected TKey _idRef = default!; #endregion } } diff --git a/src/ThunderDesign.Net-PCL.Threading/Extentions/INotifyCollectionChangedExtension.cs b/src/ThunderDesign.Net-PCL.Threading/Extentions/INotifyCollectionChangedExtension.cs index b806387..620d613 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Extentions/INotifyCollectionChangedExtension.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Extentions/INotifyCollectionChangedExtension.cs @@ -7,7 +7,7 @@ public static class INotifyCollectionChangedExtension { public static void NotifyCollectionChanged( this INotifyCollectionChanged sender, - NotifyCollectionChangedEventHandler handler, + NotifyCollectionChangedEventHandler? handler, NotifyCollectionChangedEventArgs args, bool notifyAndWait = true) { diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IBindableDataObject.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IBindableDataObject.cs index 05a650f..bbfac70 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IBindableDataObject.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IBindableDataObject.cs @@ -4,7 +4,7 @@ public interface IBindableDataObject : IDataObject, IBindableObject { } - public interface IBindableDataObject : IDataObject, IBindableDataObject + public interface IBindableDataObject : IDataObject, IBindableDataObject where TKey : notnull { } } diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IDataObject.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IDataObject.cs index deab422..33cfdb9 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IDataObject.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IDataObject.cs @@ -7,10 +7,10 @@ public interface IDataObject #endregion } - public interface IDataObject : IDataObject + public interface IDataObject : IDataObject where TKey : notnull { #region properties - new Key Id { get; set; } + new TKey Id { get; set; } #endregion } } diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IDictionaryThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IDictionaryThreadSafe.cs index ea54a8e..fc19e1e 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IDictionaryThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IDictionaryThreadSafe.cs @@ -14,14 +14,19 @@ public interface IDictionaryThreadSafe : IDictionary { } - public interface IDictionaryThreadSafe : IDictionary, IReadOnlyDictionary, IDictionaryThreadSafe + public interface IDictionaryThreadSafe : IDictionary, IReadOnlyDictionary, IDictionaryThreadSafe where TKey : notnull { new TValue this[TKey key] { get; set; } new int Count { get; } new void Add(TKey key, TValue value); new bool ContainsKey(TKey key); new bool Remove(TKey key); +#if NET6_0_OR_GREATER + new bool TryGetValue(TKey key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TValue value); +#else new bool TryGetValue(TKey key, out TValue value); +#endif + new void Clear(); } } diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IGateKeeper.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IGateKeeper.cs new file mode 100644 index 0000000..af5e7c9 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IGateKeeper.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ThunderDesign.Net.Threading.Interfaces +{ + public interface IGateKeeper : IDisposable + { + bool IsGateOpen { get; } + + long LockedGateableThreadLocks { get; } + + void OpenGate(); + + Task CloseGateAsync(CancellationToken cancellationToken = default); + } + + public interface IGateKeeper : IGateKeeper where TKey : notnull where TValue : IGatedThreadLock + { + IGatedThreadLock GetOrAdd(TKey key); + + void Remove(TKey key); + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IGatedThreadLock.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IGatedThreadLock.cs new file mode 100644 index 0000000..2187c95 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IGatedThreadLock.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ThunderDesign.Net.Threading.Interfaces +{ + public interface IGatedThreadLock : ISingleThreadLock, IDisposable + { + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IHashSetThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IHashSetThreadSafe.cs index f375922..ae86a4a 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IHashSetThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IHashSetThreadSafe.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER using System.Runtime.Serialization; #endif @@ -40,7 +42,14 @@ public interface IHashSetThreadSafe : ICollection, IEnumerable, IReadOn new void SymmetricExceptWith(IEnumerable other); new void UnionWith(IEnumerable other); #if NET8_0_OR_GREATER - bool TryGetValue(T equalValue, out T actualValue); + bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue); +#endif +#if NET6_0_OR_GREATER + int EnsureCapacity(int capacity); + void TrimExcess(); +#endif +#if NET10_0_OR_GREATER + void TrimExcess(int capacity); #endif #endregion } diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/ILinkedListThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/ILinkedListThreadSafe.cs index 8d3edb4..03b7653 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/ILinkedListThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/ILinkedListThreadSafe.cs @@ -17,8 +17,8 @@ public interface ILinkedListThreadSafe : IEnumerable, ICollection { #region properties new int Count { get; } - LinkedListNode First { get; } - LinkedListNode Last { get; } + LinkedListNode? First { get; } + LinkedListNode? Last { get; } #endregion #region methods @@ -33,8 +33,8 @@ public interface ILinkedListThreadSafe : IEnumerable, ICollection new void Clear(); new bool Contains(T value); new void CopyTo(T[] array, int index); - LinkedListNode Find(T value); - LinkedListNode FindLast(T value); + LinkedListNode? Find(T value); + LinkedListNode? FindLast(T value); void Remove(LinkedListNode node); new bool Remove(T value); void RemoveFirst(); diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMethodThreadLock.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMethodThreadLock.cs new file mode 100644 index 0000000..b01f34e --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMethodThreadLock.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Locks; + +namespace ThunderDesign.Net.Threading.Interfaces +{ + public interface IMethodThreadLock : IDisposable + { + Task LockAsync([CallerMemberName] string? methodName = null, CancellationToken cancellationToken = default); + + void Unlock([CallerMemberName] string? methodName = null); + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMultipleThreadLockManager.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMultipleThreadLockManager.cs new file mode 100644 index 0000000..cc6a84b --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IMultipleThreadLockManager.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ThunderDesign.Net.Threading.Interfaces +{ + public interface IMultipleThreadLockManager : IDisposable where TKey : notnull + { + Task LockAsync(TKey key, CancellationToken cancellationToken = default); + + void Unlock(TKey key); + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IObservableDataDictionary.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IObservableDataDictionary.cs index b60b168..ebdf04e 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IObservableDataDictionary.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IObservableDataDictionary.cs @@ -4,7 +4,7 @@ public interface IObservableDataDictionary : IObservableDictionaryThreadSafe { } - public interface IObservableDataDictionary : IObservableDictionaryThreadSafe, IObservableDataDictionary + public interface IObservableDataDictionary : IObservableDictionaryThreadSafe, IObservableDataDictionary where TKey : notnull { #region methods void Add(TValue value); diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IObservableDictionaryThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IObservableDictionaryThreadSafe.cs index ee1d7a7..1d18c05 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IObservableDictionaryThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IObservableDictionaryThreadSafe.cs @@ -8,7 +8,7 @@ public interface IObservableDictionaryThreadSafe : IDictionaryThreadSafe, IBinda void Reset(); } - public interface IObservableDictionaryThreadSafe : IDictionaryThreadSafe, IObservableDictionaryThreadSafe + public interface IObservableDictionaryThreadSafe : IDictionaryThreadSafe, IObservableDictionaryThreadSafe where TKey : notnull { } } diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/ISingleThreadLock.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/ISingleThreadLock.cs new file mode 100644 index 0000000..852010e --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/ISingleThreadLock.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ThunderDesign.Net.Threading.Interfaces +{ + public interface ISingleThreadLock : IDisposable + { + bool IsLocked { get; } + Task LockAsync(CancellationToken cancellationToken = default); + void Unlock(); + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/ISortedDictionaryThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/ISortedDictionaryThreadSafe.cs index 57f1d69..f7a8d69 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/ISortedDictionaryThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/ISortedDictionaryThreadSafe.cs @@ -7,12 +7,12 @@ namespace ThunderDesign.Net.Threading.Interfaces { #if NET8_0_OR_GREATER - public interface ISortedDictionaryThreadSafe : IDictionary, IDictionary, IReadOnlyDictionary + public interface ISortedDictionaryThreadSafe : IDictionary, IDictionary, IReadOnlyDictionary where TKey : notnull #elif NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER - public interface ISortedDictionaryThreadSafe : ICollection>, IEnumerable>, IEnumerable, IDictionary, IReadOnlyCollection>, IReadOnlyDictionary, ICollection, IDictionary + public interface ISortedDictionaryThreadSafe : ICollection>, IEnumerable>, IEnumerable, IDictionary, IReadOnlyCollection>, IReadOnlyDictionary, ICollection, IDictionary where TKey : notnull #else - public interface ISortedDictionaryThreadSafe : IDictionary + public interface ISortedDictionaryThreadSafe : IDictionary where TKey : notnull #endif { #region properties @@ -29,7 +29,11 @@ public interface ISortedDictionaryThreadSafe : IDictionary : IDictionary, ISortedListThreadSafe + public interface ISortedListThreadSafe : IDictionary, ISortedListThreadSafe where TKey : notnull { } } diff --git a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IStackThreadSafe.cs b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IStackThreadSafe.cs index 6ca528f..32430ab 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Interfaces/IStackThreadSafe.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Interfaces/IStackThreadSafe.cs @@ -27,8 +27,8 @@ public interface IStackThreadSafe : IEnumerable, System.Collections.IColle T[] ToArray(); void TrimExcess(); #if NET6_0_OR_GREATER - bool TryPeek(out T result); - bool TryPop(out T result); + bool TryPeek([System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out T result); + bool TryPop([System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out T result); #endif #endregion } diff --git a/src/ThunderDesign.Net-PCL.Threading/Locks/GateKeeper.cs b/src/ThunderDesign.Net-PCL.Threading/Locks/GateKeeper.cs new file mode 100644 index 0000000..93188b0 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Locks/GateKeeper.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net.Threading.Extentions; +using ThunderDesign.Net.Threading.Interfaces; +using ThunderDesign.Net.Threading.Objects; + +namespace ThunderDesign.Net.Threading.Locks +{ + internal interface IInternalGateKeeperLink + { + internal Task EnterGateAsync(CancellationToken cancellationToken = default); + internal void ExitGate(); + } + + public class GateKeeper : DisposableThreadObject, IGateKeeper, IInternalGateKeeperLink, IDisposable where TKey : notnull where TValue : IGatedThreadLock, new() + { + private bool _isGateOpen = true; + + // Using Interlocked to ensure thread-safe access to the _lockedGateableThreadLocks field + private long _lockedGateableThreadLocks = 0; + + private TaskCompletionSource? _closeGateReadyTcs = null; + + protected readonly DictionaryThreadSafe GateableThreadLocks = new DictionaryThreadSafe(); + + protected readonly SemaphoreSlim MasterGateLock = new SemaphoreSlim(1, 1); + + public bool IsGateOpen + { + get => this.GetProperty(ref _isGateOpen, _Locker); + private set => this.SetProperty(ref _isGateOpen, value, _Locker); + } + + public long LockedGateableThreadLocks => Interlocked.Read(ref _lockedGateableThreadLocks); + + protected TaskCompletionSource? CloseGateReadyTcs + { + get => this.GetProperty(ref _closeGateReadyTcs, _Locker); + set + { + lock (_Locker) + { + var oldValue = _closeGateReadyTcs; + if (this.SetProperty(ref _closeGateReadyTcs, value)) + { + oldValue?.TrySetResult(false); + } + } + } + } + + public async Task CloseGateAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + using var linkedCts = LinkTokenSourceDisposing(cancellationToken); + + var lockAcquired = false; + try + { + await MasterGateLock.WaitAsync(linkedCts.Token).ConfigureAwait(false); + lockAcquired = true; + + + if (LockedGateableThreadLocks > 0) + { +#if NETSTANDARD1_0 + CloseGateReadyTcs = new TaskCompletionSource(); +#else + CloseGateReadyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); +#endif + + if (LockedGateableThreadLocks == 0) + { + CloseGateReadyTcs = null; + IsGateOpen = false; + return; + } + + await Task.WhenAny(CloseGateReadyTcs?.Task ?? Task.FromResult(false), Task.Delay(Timeout.Infinite, linkedCts.Token)).ConfigureAwait(false); + if (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(cancellationToken); + } + if (!((CloseGateReadyTcs?.Task.IsCompleted == true) && (CloseGateReadyTcs?.Task.Result ?? false == true))) + { + throw new OperationCanceledException("The gate closing operation was canceled by another operation."); + } + } + + IsGateOpen = false; + } + catch (Exception ex) + { + if (lockAcquired) + MasterGateLock?.Release(); + + switch (ex) + { + case OperationCanceledException: + ThrowIfDisposed(); + throw; + default: + throw; + } + } + } + + public IGatedThreadLock GetOrAdd(TKey key) + { + ThrowIfDisposed(); + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (GateableThreadLocks.TryGetValue(key, out var existingLock)) + { + return existingLock; + } + else + { + var newLock = new TValue(); + if (newLock is IInternalGatedThreadLockLink internalLock) + { + internalLock.LinkGateKeeper(this); + } + else + { + throw new InvalidOperationException("The provided gateable thread lock does not inherit from GatedThreadLock Class."); + } + GateableThreadLocks.Add(key, newLock); + return newLock; + } + } + + public void OpenGate() + { + ThrowIfDisposed(); + + if (IsGateOpen) + { + throw new InvalidOperationException("The gate is already open."); + } + + IsGateOpen = true; + CloseGateReadyTcs = null; + MasterGateLock?.Release(); + } + + public void Remove(TKey key) + { + ThrowIfDisposed(); + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (GateableThreadLocks.TryGetValue(key, out var existingLock)) + { + GateableThreadLocks.Remove(key); + if (existingLock is IInternalGatedThreadLockLink internalLock) + { + internalLock.UnlinkGateKeeper(this); + } + else + { + throw new InvalidOperationException("The provided gateable thread lock does not inherit from GatedThreadLock Class."); + } + } + else + { + throw new KeyNotFoundException($"The specified key: {key} does not exist in the gateable thread locks collection."); + } + } + + void IInternalGateKeeperLink.ExitGate() => ExitGate(); + + private void ExitGate() + { + ThrowIfDisposed(); + + if (Interlocked.Decrement(ref _lockedGateableThreadLocks) == 0) + { + CloseGateReadyTcs?.TrySetResult(true); + } + } + + Task IInternalGateKeeperLink.EnterGateAsync(CancellationToken cancellationToken) => EnterGateAsync(cancellationToken); + + private async Task EnterGateAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + using var linkedCts = LinkTokenSourceDisposing(cancellationToken); + + try + { + await MasterGateLock.WaitAsync(linkedCts.Token).ConfigureAwait(false); + try + { + Interlocked.Increment(ref _lockedGateableThreadLocks); + } + finally + { + MasterGateLock?.Release(); + } + } + catch (OperationCanceledException) + { + ThrowIfDisposed(); + throw; + } + } + + protected override void Dispose(bool disposing, bool setDisposed = true) + { + base.Dispose(disposing, setDisposed: false); + if (!Disposed) + { + if (disposing) + { + // Dispose managed resources here + foreach (var threadLock in GateableThreadLocks.Values) + { + threadLock?.Dispose(); + } + + GateableThreadLocks.Clear(); + } + + // Dispose unmanaged resources here + + if (setDisposed) + { + Disposed = true; + } + } + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Locks/GatedThreadLock.cs b/src/ThunderDesign.Net-PCL.Threading/Locks/GatedThreadLock.cs new file mode 100644 index 0000000..83847b6 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Locks/GatedThreadLock.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Interfaces; + +namespace ThunderDesign.Net.Threading.Locks +{ + internal interface IInternalGatedThreadLockLink + { + internal void LinkGateKeeper(IInternalGateKeeperLink gateKeeperLink); + internal void UnlinkGateKeeper(IInternalGateKeeperLink gateKeeperLink); + } + + public class GatedThreadLock : SingleThreadLock, IGatedThreadLock, IInternalGatedThreadLockLink, IDisposable + { + private IInternalGateKeeperLink? _gateKeeperLink; + + void IInternalGatedThreadLockLink.LinkGateKeeper(IInternalGateKeeperLink gateKeeperLink) + { + LinkGateKeeper(gateKeeperLink); + } + + private void LinkGateKeeper(IInternalGateKeeperLink gateKeeperLink) + { + if (gateKeeperLink == null) + throw new ArgumentNullException(nameof(gateKeeperLink)); + + _gateKeeperLink = gateKeeperLink; + } + + void IInternalGatedThreadLockLink.UnlinkGateKeeper(IInternalGateKeeperLink gateKeeperLink) + { + UnlinkGateKeeper(gateKeeperLink); + } + + private void UnlinkGateKeeper(IInternalGateKeeperLink gateKeeperLink) + { + if (gateKeeperLink == null) + throw new ArgumentNullException(nameof(gateKeeperLink)); + + if (_gateKeeperLink != gateKeeperLink) + throw new InvalidOperationException("The provided lock manager is not registered with this lock."); + + _gateKeeperLink = null; + } + + public override async Task LockAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + using var linkedCts = LinkTokenSourceDisposing(cancellationToken); + + var lockAcquired = false; + try + { + await InternalLockWaitAsync(linkedCts.Token).ConfigureAwait(false); + lockAcquired = true; + + if (_gateKeeperLink == null) + throw new InvalidOperationException("The gate keeper link is not set."); + + await _gateKeeperLink.EnterGateAsync(linkedCts.Token).ConfigureAwait(false); + } + catch (Exception ex) + { + if (lockAcquired) + InternalLockRelease(); + + switch (ex) + { + case OperationCanceledException: + ThrowIfDisposed(); + throw; + default: + throw; + } + } + } + + public override void Unlock() + { + base.Unlock(); + _gateKeeperLink?.ExitGate(); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Locks/MethodThreadLock.cs b/src/ThunderDesign.Net-PCL.Threading/Locks/MethodThreadLock.cs new file mode 100644 index 0000000..e0124a2 --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Locks/MethodThreadLock.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Interfaces; + +namespace ThunderDesign.Net.Threading.Locks +{ + public class MethodThreadLock : MultipleThreadLockManager, IMethodThreadLock, IDisposable + { + public override async Task LockAsync([CallerMemberName] string? methodName = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(methodName)) + throw new ArgumentNullException(nameof(methodName)); + + await base.LockAsync(methodName!, cancellationToken).ConfigureAwait(false); + } + + public override void Unlock([CallerMemberName] string? methodName = null) + { + if (string.IsNullOrWhiteSpace(methodName)) + throw new ArgumentNullException(nameof(methodName)); + + base.Unlock(methodName!); + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Locks/MultipleThreadLockManager.cs b/src/ThunderDesign.Net-PCL.Threading/Locks/MultipleThreadLockManager.cs new file mode 100644 index 0000000..b05841f --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Locks/MultipleThreadLockManager.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Collections; +using ThunderDesign.Net.Threading.Extentions; +using ThunderDesign.Net.Threading.Interfaces; +using ThunderDesign.Net.Threading.Objects; + +namespace ThunderDesign.Net.Threading.Locks +{ + public class MultipleThreadLockManager : DisposableThreadObject, IMultipleThreadLockManager where TKey : notnull where TValue : ISingleThreadLock, new() + { + protected readonly DictionaryThreadSafe ThreadLocks = new DictionaryThreadSafe(); + + public virtual async Task LockAsync(TKey key, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + if (key == null) + throw new ArgumentNullException(nameof(key)); + + var threadLock = ThreadLocks.GetOrAdd(key, _ => new TValue()); + + using var lockCts = LinkTokenSourceDisposing(cancellationToken); + + try + { + await threadLock.LockAsync(lockCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + ThrowIfDisposed(); + throw; + } + } + + public virtual void Unlock(TKey key) + { + ThrowIfDisposed(); + + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (ThreadLocks.TryGetValue(key, out var threadLock)) + { + if (threadLock?.IsLocked ?? false) + threadLock.Unlock(); + else + throw new SynchronizationLockException($"The lock for the specified key: {key} is not currently locked."); + } + else + { + throw new KeyNotFoundException($"No lock found for the specified key: {key}"); + } + } + + protected override void Dispose(bool disposing, bool setDisposed = true) + { + base.Dispose(disposing, setDisposed: false); + if (!Disposed) + { + if (disposing) + { + // Dispose managed resources here + foreach (var threadLock in ThreadLocks.Values) + { + threadLock?.Dispose(); + } + + ThreadLocks.Clear(); + } + + // Dispose unmanaged resources here + + if (setDisposed) + { + Disposed = true; + } + } + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Locks/SingleThreadLock.cs b/src/ThunderDesign.Net-PCL.Threading/Locks/SingleThreadLock.cs new file mode 100644 index 0000000..2eb2c8e --- /dev/null +++ b/src/ThunderDesign.Net-PCL.Threading/Locks/SingleThreadLock.cs @@ -0,0 +1,94 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using ThunderDesign.Net.Threading.Extentions; +using ThunderDesign.Net.Threading.Interfaces; +using ThunderDesign.Net.Threading.Objects; + +namespace ThunderDesign.Net.Threading.Locks +{ + + public class SingleThreadLock : DisposableThreadObject, ISingleThreadLock, IDisposable + { + // Using Interlocked to ensure thread-safe access to the _lockedGateableThreadLocks field + private int _pendingWaitCount; + + private readonly SemaphoreSlim _internalLock = new SemaphoreSlim(1, 1); + + public bool IsLocked => _internalLock?.CurrentCount == 0; + + public virtual async Task LockAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + using var linkedCts = LinkTokenSourceDisposing(cancellationToken); + + try + { + await InternalLockWaitAsync(linkedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + ThrowIfDisposed(); + throw; + } + } + + public virtual void Unlock() + { + ThrowIfDisposed(); + + if (!IsLocked) + throw new SynchronizationLockException("The lock is not currently held."); + + InternalLockRelease(); + } + + protected void InternalLockDispose() + { + // Spin until all threads have decremented the counter and exited the block or until the timeout is reached (1 second) + SpinWait.SpinUntil(() => Volatile.Read(ref _pendingWaitCount) == 0, 1000); + if (IsLocked) + _internalLock?.Release(); + _internalLock?.Dispose(); + } + + protected async Task InternalLockWaitAsync(CancellationToken cancellationToken) + { + Interlocked.Increment(ref _pendingWaitCount); + try + { + await _internalLock.WaitAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + Interlocked.Decrement(ref _pendingWaitCount); + } + } + + protected void InternalLockRelease() + { + _internalLock?.Release(); + } + + protected override void Dispose(bool disposing, bool setDisposed = true) + { + base.Dispose(disposing, setDisposed: false); + if (!Disposed) + { + if (disposing) + { + InternalLockDispose(); + } + + // Dispose unmanaged resources here + + if (setDisposed) + { + Disposed = true; + } + } + } + } +} diff --git a/src/ThunderDesign.Net-PCL.Threading/Objects/BindableObject.cs b/src/ThunderDesign.Net-PCL.Threading/Objects/BindableObject.cs index ab5a2e7..bc7b552 100644 --- a/src/ThunderDesign.Net-PCL.Threading/Objects/BindableObject.cs +++ b/src/ThunderDesign.Net-PCL.Threading/Objects/BindableObject.cs @@ -17,7 +17,7 @@ public BindableObject(bool waitOnNotifying = true) : base() } #endregion #region event handlers - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; #endregion #region properties 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 d237efc..07bf8d8 100644 --- a/src/ThunderDesign.Net-PCL.Threading/ThunderDesign.Net-PCL.Threading.csproj +++ b/src/ThunderDesign.Net-PCL.Threading/ThunderDesign.Net-PCL.Threading.csproj @@ -1,12 +1,14 @@  - netstandard1.0;netstandard1.3;netstandard2.0;net461;net6.0;net8.0;net10.0 + netstandard1.0;netstandard1.3;netstandard2.0;net6.0;net8.0;net9.0;net10.0 + latest + enable ThunderDesign.Net.Threading README.md - - NETSTANDARD1_4 + + $(NoWarn);1903 @@ -15,11 +17,7 @@ - +