From c08a6283b8163f2e9d5101142129b3376bd8a7ed Mon Sep 17 00:00:00 2001 From: Drew Tingen Date: Tue, 28 Sep 2021 09:32:07 -0400 Subject: [PATCH 1/7] fix: Fixed EnumUtils.GetValuesExceptNone to work for non-flag enums --- ICD.Common.Utils.Tests/EnumUtilsTest.cs | 3 ++- ICD.Common.Utils/EnumUtils.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ICD.Common.Utils.Tests/EnumUtilsTest.cs b/ICD.Common.Utils.Tests/EnumUtilsTest.cs index 44daa69..fa60630 100644 --- a/ICD.Common.Utils.Tests/EnumUtilsTest.cs +++ b/ICD.Common.Utils.Tests/EnumUtilsTest.cs @@ -22,7 +22,8 @@ namespace ICD.Common.Utils.Tests A = 1, B = 2, C = 4, - D = 32 + D = 32, + BandC = B | C } [Test] diff --git a/ICD.Common.Utils/EnumUtils.cs b/ICD.Common.Utils/EnumUtils.cs index 9de5d1e..1f16c0c 100644 --- a/ICD.Common.Utils/EnumUtils.cs +++ b/ICD.Common.Utils/EnumUtils.cs @@ -198,7 +198,7 @@ namespace ICD.Common.Utils if (type == null) throw new ArgumentNullException("type"); - return GetFlagsExceptNone(type); + return GetValues(type).Where(v => (int)v != 0); } /// From 63d76d8cef3d1fd9ee834740152f493575c4fb1d Mon Sep 17 00:00:00 2001 From: Drew Tingen Date: Thu, 23 Sep 2021 17:33:15 -0400 Subject: [PATCH 2/7] feat: Adding IcdAutoResetEvent and IcdMaunalResetEvent --- .../IcdAutoResetEventTest.cs | 188 ++++++++++++++++++ .../IcdManualResetEventTest.cs | 183 +++++++++++++++++ ICD.Common.Utils/AbstractIcdResetEvent.cs | 106 ++++++++++ .../ICD.Common.Utils_SimplSharp.csproj | 3 + ICD.Common.Utils/IcdAutoResetEvent.cs | 39 ++++ ICD.Common.Utils/IcdManualResetEvent.cs | 40 ++++ 6 files changed, 559 insertions(+) create mode 100644 ICD.Common.Utils.Tests/IcdAutoResetEventTest.cs create mode 100644 ICD.Common.Utils.Tests/IcdManualResetEventTest.cs create mode 100644 ICD.Common.Utils/AbstractIcdResetEvent.cs create mode 100644 ICD.Common.Utils/IcdAutoResetEvent.cs create mode 100644 ICD.Common.Utils/IcdManualResetEvent.cs diff --git a/ICD.Common.Utils.Tests/IcdAutoResetEventTest.cs b/ICD.Common.Utils.Tests/IcdAutoResetEventTest.cs new file mode 100644 index 0000000..75b852b --- /dev/null +++ b/ICD.Common.Utils.Tests/IcdAutoResetEventTest.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; + +namespace ICD.Common.Utils.Tests +{ + [TestFixture] + class IcdAutoResetEventTest + { + [TestCase(true)] + [TestCase(false)] + public void InitialStateTest(bool initialState) + { + using (var waitHandleEvent = new IcdAutoResetEvent(initialState)) + { + Assert.AreEqual(initialState, waitHandleEvent.WaitOne(1), "Initial state incorrect"); + } + } + + [Test] + public void DefaultInitialStateTest() + { + using (var waitHandleEvent = new IcdAutoResetEvent()) + { + Assert.False(waitHandleEvent.WaitOne(1), "Initial state incorrect"); + } + } + + [TestCase(1)] + [TestCase(5)] + [TestCase(15)] + public void MultipleThreadSetTest(int count) + { + int releasedCount = 0; + + SafeCriticalSection countSection = new SafeCriticalSection(); + + + IcdAutoResetEvent waitHandleEvent = new IcdAutoResetEvent(false); + + for (int i = 0; i < count; i++) + ThreadingUtils.SafeInvoke(() => + { + waitHandleEvent.WaitOne(); + countSection.Execute(() => releasedCount++); + ; + }); + + Assert.AreEqual(0, countSection.Execute(() => releasedCount), "Threads released early"); + + ///Auto reset should only release one thread at a time + for (int i = 1; i <= count; i++) + { + waitHandleEvent.Set(); + + ThreadingUtils.Sleep(100); + + Assert.AreEqual(i, countSection.Execute(() => releasedCount), + "Incorrect number of threads released"); + } + + waitHandleEvent.Dispose(); + } + + [TestCase(1)] + [TestCase(5)] + [TestCase(15)] + public void MultipleThreadResetTest(int count) + { + int releasedCount = 0; + + SafeCriticalSection countSection = new SafeCriticalSection(); + + + IcdAutoResetEvent waitHandleEvent = new IcdAutoResetEvent(true); + + Assert.True(waitHandleEvent.WaitOne(1), "Initial State Wrong"); + + waitHandleEvent.Reset(); + + for (int i = 0; i < count; i++) + ThreadingUtils.SafeInvoke(() => + { + if (waitHandleEvent.WaitOne(100)) + countSection.Execute(() => releasedCount++); + }); + + ThreadingUtils.Sleep(2000); + + Assert.AreEqual(0, countSection.Execute(() => releasedCount), "Incorrect number of threads released"); + + waitHandleEvent.Set(); + + Assert.AreEqual(0, countSection.Execute(() => releasedCount), "Threads released after timeout"); + + waitHandleEvent.Dispose(); + } + + [TestCase(1)] + [TestCase(200)] + [TestCase(5000)] + public void WaitOneTest(int waitTime) + { + bool released = false; + + IcdAutoResetEvent waitHandleEvent = new IcdAutoResetEvent(false); + + ThreadingUtils.SafeInvoke(() => + { + waitHandleEvent.WaitOne(); + released = true; + }); + + ThreadingUtils.Sleep(waitTime); + + Assert.False(released, "Thread released when it shouldn't have"); + + waitHandleEvent.Set(); + + ThreadingUtils.Sleep(100); + + Assert.True(released, "Thread didn't release after set event"); + + waitHandleEvent.Dispose(); + } + + [TestCase(200)] + [TestCase(500)] + [TestCase(5000)] + public void WaitOneTimeoutTest(int waitTime) + { + bool released = false; + + IcdAutoResetEvent waitHandleEvent = new IcdAutoResetEvent(false); + + ThreadingUtils.SafeInvoke(() => + { + waitHandleEvent.WaitOne(waitTime * 2); + released = true; + }); + + ThreadingUtils.Sleep(waitTime); + + Assert.False(released, "Thread released when it shouldn't have"); + + waitHandleEvent.Set(); + + ThreadingUtils.Sleep(100); + + Assert.True(released, "Thread didn't release after set event"); + + waitHandleEvent.Dispose(); + } + + [TestCase(200)] + [TestCase(500)] + [TestCase(5000)] + public void WaitOneTimedOutTest(int waitTime) + { + bool released = false; + bool? returned = null; + + IcdAutoResetEvent waitHandleEvent = new IcdAutoResetEvent(false); + + ThreadingUtils.SafeInvoke(() => + { + returned = waitHandleEvent.WaitOne(waitTime); + released = true; + + }); + + ThreadingUtils.Sleep(100); + + Assert.True(returned == null, "WaitOne returned when it shouldn't have"); + Assert.False(released, "Thread released when it shouldn't have"); + + ThreadingUtils.Sleep(waitTime * 2); + + Assert.True(released, "Thread didn't release after timeout"); + Assert.True(returned.HasValue && returned.Value == false, "WaitOne timeout didn't return false"); + + waitHandleEvent.Set(); + + waitHandleEvent.Dispose(); + } + } +} diff --git a/ICD.Common.Utils.Tests/IcdManualResetEventTest.cs b/ICD.Common.Utils.Tests/IcdManualResetEventTest.cs new file mode 100644 index 0000000..1380512 --- /dev/null +++ b/ICD.Common.Utils.Tests/IcdManualResetEventTest.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; + +namespace ICD.Common.Utils.Tests +{ + [TestFixture] + class IcdManualResetEventTest + { + [TestCase(true)] + [TestCase(false)] + public void InitialStateTest(bool initialState) + { + using (var waitHandleEvent = new IcdManualResetEvent(initialState)) + { + Assert.AreEqual(initialState, waitHandleEvent.WaitOne(1), "Initial state incorrect"); + } + } + + [Test] + public void DefaultInitialStateTest() + { + using (var waitHandleEvent = new IcdManualResetEvent()) + { + Assert.False(waitHandleEvent.WaitOne(1), "Initial state incorrect"); + } + } + + [TestCase(1)] + [TestCase(5)] + [TestCase(15)] + public void MultipleThreadSetTest(int count) + { + int releasedCount = 0; + + SafeCriticalSection countSection = new SafeCriticalSection(); + + + IcdManualResetEvent waitHandleEvent = new IcdManualResetEvent(false); + + for (int i = 0; i < count; i++) + ThreadingUtils.SafeInvoke(() => + { + waitHandleEvent.WaitOne(); + countSection.Execute(() => releasedCount++); + ; + }); + + Assert.AreEqual(0, countSection.Execute(() => releasedCount), "Threads released early"); + + waitHandleEvent.Set(); + + ThreadingUtils.Sleep(500); + + Assert.AreEqual(count, countSection.Execute(() => releasedCount), "Incorrect number of threads released"); + + waitHandleEvent.Dispose(); + } + + [TestCase(1)] + [TestCase(5)] + [TestCase(15)] + public void MultipleThreadResetTest(int count) + { + int releasedCount = 0; + + SafeCriticalSection countSection = new SafeCriticalSection(); + + + IcdManualResetEvent waitHandleEvent = new IcdManualResetEvent(true); + + Assert.True(waitHandleEvent.WaitOne(1), "Initial State Wrong"); + + waitHandleEvent.Reset(); + + for (int i = 0; i < count; i++) + ThreadingUtils.SafeInvoke(() => + { + if (waitHandleEvent.WaitOne(100)) + countSection.Execute(() => releasedCount++); + }); + + ThreadingUtils.Sleep(2000); + + Assert.AreEqual(0, countSection.Execute(() => releasedCount), "Incorrect number of threads released"); + + waitHandleEvent.Set(); + + Assert.AreEqual(0, countSection.Execute(() => releasedCount), "Threads released after timeout"); + + waitHandleEvent.Dispose(); + } + + [TestCase(1)] + [TestCase(200)] + [TestCase(5000)] + public void WaitOneTest(int waitTime) + { + bool released = false; + + IcdManualResetEvent waitHandleEvent = new IcdManualResetEvent(false); + + ThreadingUtils.SafeInvoke(() => + { + waitHandleEvent.WaitOne(); + released = true; + }); + + ThreadingUtils.Sleep(waitTime); + + Assert.False(released, "Thread released when it shouldn't have"); + + waitHandleEvent.Set(); + + ThreadingUtils.Sleep(100); + + Assert.True(released, "Thread didn't release after set event"); + + waitHandleEvent.Dispose(); + } + + [TestCase(200)] + [TestCase(500)] + [TestCase(5000)] + public void WaitOneTimeoutTest(int waitTime) + { + bool released = false; + + IcdManualResetEvent waitHandleEvent = new IcdManualResetEvent(false); + + ThreadingUtils.SafeInvoke(() => + { + waitHandleEvent.WaitOne(waitTime * 2); + released = true; + }); + + ThreadingUtils.Sleep(waitTime); + + Assert.False(released, "Thread released when it shouldn't have"); + + waitHandleEvent.Set(); + + ThreadingUtils.Sleep(100); + + Assert.True(released, "Thread didn't release after set event"); + + waitHandleEvent.Dispose(); + } + + [TestCase(200)] + [TestCase(500)] + [TestCase(5000)] + public void WaitOneTimedOutTest(int waitTime) + { + bool released = false; + bool? returned = null; + + IcdManualResetEvent waitHandleEvent = new IcdManualResetEvent(false); + + ThreadingUtils.SafeInvoke(() => + { + returned = waitHandleEvent.WaitOne(waitTime); + released = true; + + }); + + ThreadingUtils.Sleep(100); + + Assert.True(returned == null, "WaitOne returned when it shouldn't have"); + Assert.False(released, "Thread released when it shouldn't have"); + + ThreadingUtils.Sleep(waitTime * 2); + + Assert.True(released, "Thread didn't release after timeout"); + Assert.True(returned.HasValue && returned.Value == false, "WaitOne timeout didn't return false"); + + waitHandleEvent.Set(); + + waitHandleEvent.Dispose(); + } + } +} diff --git a/ICD.Common.Utils/AbstractIcdResetEvent.cs b/ICD.Common.Utils/AbstractIcdResetEvent.cs new file mode 100644 index 0000000..4d9fca4 --- /dev/null +++ b/ICD.Common.Utils/AbstractIcdResetEvent.cs @@ -0,0 +1,106 @@ +using System; +using ICD.Common.Properties; +#if SIMPLSHARP +using Crestron.SimplSharp; +#else +using System.Threading; +#endif + +namespace ICD.Common.Utils +{ + public abstract class AbstractIcdResetEvent : IDisposable + { + +#if SIMPLSHARP + private readonly CEvent m_Event; +#else + private readonly EventWaitHandle m_Event; +#endif + +#if SIMPLSHARP + /// + /// Initializes a new instance of the IcdManualResetEvent class with the CEvent + /// + [PublicAPI] + protected AbstractIcdResetEvent(CEvent eventHandle) + { + m_Event = eventHandle; + } +#else + /// + /// Initializes a new instance of the IcdManualResetEvent class with the CEvent + /// + [PublicAPI] + protected AbstractIcdResetEvent(EventWaitHandle eventHandle) + { + m_Event = eventHandle; + } + +#endif + + /// + /// Sets the state of the event to signaled, allowing one or more waiting threads to proceed. + /// + /// true if the operation succeeds; otherwise, false. + [PublicAPI] + public bool Set() + { + return m_Event.Set(); + } + + /// + /// Sets the state of the event to nonsignaled, causing threads to block. + /// + /// true if the operation succeeds; otherwise, false. + [PublicAPI] + public bool Reset() + { + return m_Event.Reset(); + } + + /// + /// Function to wait for the event to be signaled. This will block indefinitely until the event is signaled. + /// + /// True if the current instance receives a signal otherwise false. + [PublicAPI] + public bool WaitOne() + { +#if SIMPLSHARP + return m_Event.Wait(); +#else + return m_Event.WaitOne(); +#endif + } + + /// + /// Function to wait for the event to be signaled. + /// + /// Timeout in milliseconds or Timeout.Infinite to wait indefinitely. + /// True if the current instance receives a signal otherwise false. + public bool WaitOne(int timeout) + { +#if SIMPLSHARP + return m_Event.Wait(timeout); +#else + return m_Event.WaitOne(timeout); +#endif + } + + /// + /// Clean up of resources. + /// + public void Dispose() + { + m_Event.Dispose(); + } + + /// + /// Close the event to release all resources used by this instance. + /// + [PublicAPI] + public void Close() + { + m_Event.Close(); + } + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index 9124253..75c17bb 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -121,6 +121,9 @@ + + + diff --git a/ICD.Common.Utils/IcdAutoResetEvent.cs b/ICD.Common.Utils/IcdAutoResetEvent.cs new file mode 100644 index 0000000..4eac0c5 --- /dev/null +++ b/ICD.Common.Utils/IcdAutoResetEvent.cs @@ -0,0 +1,39 @@ +using ICD.Common.Properties; +#if SIMPLSHARP +using Crestron.SimplSharp; +# else +using System.Threading; +#endif + +namespace ICD.Common.Utils +{ + /// + /// Notifies one or more waiting threads that an event has occurred. Every thread that passes WaitOne causes the event to be reset + /// + [PublicAPI] + public sealed class IcdAutoResetEvent : AbstractIcdResetEvent + { + + /// + /// Initializes a new instance of the IcdAutoResetEvent class with the initial state to nonsignaled. + /// + [PublicAPI] + public IcdAutoResetEvent() : this(false) + { + } + + /// + /// Initializes a new instance of the IcdManualResetEvent class with a Boolean value indicating whether to set the initial state to signaled. + /// + /// true to set the initial state signaled; false to set the initial state to nonsignaled. + [PublicAPI] + public IcdAutoResetEvent(bool initialState) : +#if SIMPLSHARP + base(new CEvent(true, initialState)) +#else + base(new AutoResetEvent(initialState)) +#endif + { + } + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/IcdManualResetEvent.cs b/ICD.Common.Utils/IcdManualResetEvent.cs new file mode 100644 index 0000000..0618555 --- /dev/null +++ b/ICD.Common.Utils/IcdManualResetEvent.cs @@ -0,0 +1,40 @@ +using ICD.Common.Properties; +#if SIMPLSHARP +using Crestron.SimplSharp; +#else +using System.Threading; +#endif + +namespace ICD.Common.Utils +{ + /// + /// Notifies one or more waiting threads that an event has occurred. + /// + [PublicAPI] + public sealed class IcdManualResetEvent : AbstractIcdResetEvent + { + + /// + /// Initializes a new instance of the IcdManualResetEvent class with the initial state to nonsignaled. + /// + [PublicAPI] + public IcdManualResetEvent() : this(false) + { + } + + /// + /// Initializes a new instance of the IcdManualResetEvent class with a Boolean value indicating whether to set the initial state to signaled. + /// + /// true to set the initial state signaled; false to set the initial state to nonsignaled. + [PublicAPI] + public IcdManualResetEvent(bool initialState) : + +#if SIMPLSHARP + base(new CEvent(false, initialState)) +#else + base(new ManualResetEvent(initialState)) +#endif + { + } + } +} \ No newline at end of file From 451cf08c0f8351fd2fc8dcd9a332fa81c0337dff Mon Sep 17 00:00:00 2001 From: Drew Tingen Date: Thu, 23 Sep 2021 17:34:20 -0400 Subject: [PATCH 3/7] fix: Fixing ThreadedWorkerQueue to not use TryEnter, and track the processing state with a bool --- .../ThreadedWorkerQueueTest.cs | 205 +++++++++++ ICD.Common.Utils/ThreadedWorkerQueue.cs | 342 ++++++++++++++++-- 2 files changed, 523 insertions(+), 24 deletions(-) create mode 100644 ICD.Common.Utils.Tests/ThreadedWorkerQueueTest.cs diff --git a/ICD.Common.Utils.Tests/ThreadedWorkerQueueTest.cs b/ICD.Common.Utils.Tests/ThreadedWorkerQueueTest.cs new file mode 100644 index 0000000..f7ccfd9 --- /dev/null +++ b/ICD.Common.Utils.Tests/ThreadedWorkerQueueTest.cs @@ -0,0 +1,205 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace ICD.Common.Utils.Tests +{ + [TestFixture] + public sealed class ThreadedWorkerQueueTest + { + [Test] + public void BetweenTimeTest() + { + List callbacks = new List(); + + using (ThreadedWorkerQueue queue = new ThreadedWorkerQueue((d) => callbacks.Add(d), true, 1000)) + { + + queue.Enqueue(10); + queue.Enqueue(20); + queue.Enqueue(30); + + ThreadingUtils.Sleep(100); + + Assert.AreEqual(1, callbacks.Count, "Initial enqueue did not trigger a dequeue"); + + ThreadingUtils.Sleep(1000); + + Assert.AreEqual(2, callbacks.Count, "Second enqueue did not dequeue"); + + ThreadingUtils.Sleep(100); + + Assert.AreEqual(2, callbacks.Count, "Third enqueue did not wait for process to complete"); + + ThreadingUtils.Sleep(1000); + + Assert.AreEqual(3, callbacks.Count); + } + } + + #region Properties + + [TestCase(1000)] + [TestCase(0)] + [TestCase(long.MaxValue)] + public void BetweenMillisecondsTest(long milliseconds) + { + using (var queue = new ThreadedWorkerQueue((d) => { }, true, milliseconds)) + Assert.AreEqual(milliseconds, queue.BetweenTime); + } + + [TestCase(5)] + [TestCase(0)] + [TestCase(30)] + public void CountTest(int count) + { + using (var queue = new ThreadedWorkerQueue(d => { }, false)) + { + for (int i = 0; i < count; i++) + queue.Enqueue(1); + + Assert.AreEqual(count, queue.Count); + } + } + + [Test] + public void ProcessBetweenTimeTest() + { + var processed = new List(); + + using (var queue = new ThreadedWorkerQueue(d => processed.Add(d), false, 1000)) + { + queue.Enqueue(1); + queue.Enqueue(2); + queue.Enqueue(3); + + ThreadingUtils.Sleep(100); + Assert.AreEqual(0, processed.Count, "Queue processed item early"); + + queue.SetRunProcess(true); + + ThreadingUtils.Sleep(100); + + Assert.AreEqual(1, processed.Count, "First item not processed"); + + ThreadingUtils.Sleep(1000); + + Assert.AreEqual(2, processed.Count, "Second item not processed"); + + queue.SetRunProcess(false); + + ThreadingUtils.Sleep(2000); + + Assert.AreEqual(2, processed.Count, "Item processed after stopping run process"); + Assert.AreEqual(1, queue.Count, "Incorrect number of items in queue"); + + // Queue lower priority item + queue.Enqueue(5, 1); + queue.SetRunProcess(true); + + ThreadingUtils.Sleep(100); + + Assert.AreEqual(3, processed.Count, "Third item not processed"); + Assert.AreEqual(5, processed[2], "Dequeued incorrect priority item"); + + ThreadingUtils.Sleep(1000); + + Assert.AreEqual(4, processed.Count, "Didn't process all items"); + Assert.True(processed.SequenceEqual(new[] { 1, 2, 5, 3 }), "Processed sequence incorrect"); + } + } + + [Test] + public void FlushQueueBetweenTimeTest() + { + var processed = new List(); + + using (var queue = new ThreadedWorkerQueue(d => processed.Add(d), false, 1000)) + { + Assert.True(queue.WaitForFlush(1), "WaitForFlush on empty queue failed"); + + queue.Enqueue(11); + queue.Enqueue(21); + queue.Enqueue(31); + queue.Enqueue(41); + queue.Enqueue(51); + queue.Enqueue(61); + queue.Enqueue(71); + queue.Enqueue(81); + queue.Enqueue(91); + queue.Enqueue(101); + + queue.SetRunProcess(true); + + Assert.False(queue.WaitForFlush(1250), "WaitForFlush didn't time out"); + Assert.AreEqual(2, processed.Count, "Didn't process correct number of items in time frame"); + + Assert.True(queue.WaitForFlush(), "WaitForFlush failed"); + Assert.AreEqual(10, processed.Count, "Not all items processed"); + Assert.AreEqual(0, queue.Count, "Queue not empty"); + } + } + + #endregion + + #region Methods + + [TestCase(false)] + [TestCase(true)] + public void SetRunProcessTest(bool runProcess) + { + using (var queue = new ThreadedWorkerQueue(d => { }, !runProcess)) + { + Assert.AreEqual(!runProcess, queue.RunProcess, "Initial state wrong"); + queue.SetRunProcess(runProcess); + Assert.AreEqual(runProcess, queue.RunProcess, "Didn't set to correct state 1st time"); + queue.SetRunProcess(!runProcess); + Assert.AreEqual(!runProcess, queue.RunProcess, "Didn't set to correct state 2nd time"); + } + } + + [Test] + public void EnqueueTest() + { + var processed = new List(); + + using (var queue = new ThreadedWorkerQueue(d => processed.Add(d), false)) + { + queue.Enqueue(10); + queue.Enqueue(20); + queue.Enqueue(30); + + Assert.AreEqual(3, queue.Count, "First queue count wrong"); + + queue.Enqueue(40); + queue.Enqueue(50); + queue.Enqueue(60); + + Assert.AreEqual(6, queue.Count, "Second queue count wrong"); + + queue.SetRunProcess(true); + Assert.True(queue.WaitForFlush(),"Queue didn't flush after processing"); + + + Assert.True(processed.SequenceEqual(new[] { 10, 20, 30, 40, 50, 60 }), "Processed sequence wrong"); + } + } + + [Test] + public void ClearTest() + { + using (var queue = new ThreadedWorkerQueue(d => { }, false)) + { + queue.Enqueue(1); + queue.Enqueue(1); + queue.Enqueue(1); + + queue.Clear(); + + Assert.AreEqual(0, queue.Count); + } + } + + #endregion + } +} diff --git a/ICD.Common.Utils/ThreadedWorkerQueue.cs b/ICD.Common.Utils/ThreadedWorkerQueue.cs index 34db9e9..2adf16a 100644 --- a/ICD.Common.Utils/ThreadedWorkerQueue.cs +++ b/ICD.Common.Utils/ThreadedWorkerQueue.cs @@ -1,6 +1,7 @@ using System; using ICD.Common.Properties; using ICD.Common.Utils.Collections; +using ICD.Common.Utils.Timers; namespace ICD.Common.Utils { @@ -10,30 +11,127 @@ namespace ICD.Common.Utils /// them in a worker thread one at a time /// /// - public sealed class ThreadedWorkerQueue + public sealed class ThreadedWorkerQueue : IDisposable { + /// + /// Underlying Queue to hold items + /// private readonly PriorityQueue m_Queue; + + /// + /// Bool to track if a thread is currently running to dequeue and process items + /// + private bool m_ProcessRunning; + + /// + /// Critical section to lock the queue and the process running bool + /// private readonly SafeCriticalSection m_QueueSection; - private readonly SafeCriticalSection m_ProcessSection; + + /// + /// Action that should be run for every item as it is dequeued + /// private readonly Action m_ProcessAction; + /// + /// Event used to block and wait for the queue to empty + /// + private readonly IcdManualResetEvent m_FlushEvent; + + /// + /// If true, the queue will be processed. + /// + private bool m_RunProcess; + + private long m_BetweenTime; + + /// + /// Time to wait between processing items + /// If less than or equal to 0, items will be processed immediately + /// + [PublicAPI] + public long BetweenTime + { + get { return m_BetweenTime; } + set + { + if (value < 0) + throw new InvalidOperationException("BetweenTime can't be negative"); + + m_BetweenTime = value; + } + } + + /// + /// Timer used to wait the BetweenTime + /// + private readonly SafeTimer m_BetweenTimeTimer; + + /// + /// While true, the queue will be processed + /// When set to false, the queue will be be stopped after any current processing is finished + /// When set to true, the queue processing will be started in a new thread if there are any items in the queue + /// + /// + [PublicAPI] + public bool RunProcess { get { return m_RunProcess; } } + + /// + /// Gets the current count of the items in the queue + /// + public int Count + { + get { return m_QueueSection.Execute(() => m_Queue.Count); } + } + + #region Constructors + /// /// Constructor. /// /// Action to process the dequeued items public ThreadedWorkerQueue([NotNull] Action processItemAction) + : this(processItemAction, true, 0) + { + } + + /// + /// Constructor. + /// + /// Action to process the dequeued items + /// If true, queued items will be processed, if false, no processing will happen until runProcess is set + public ThreadedWorkerQueue([NotNull] Action processItemAction, bool runProcess) + : this(processItemAction, runProcess, 0) + { + } + + /// + /// Constructor. + /// + /// Action to process the dequeued items + /// If true, queued items will be processed, if false, no processing will happen until runProcess is set + /// Time to wait between processing items + public ThreadedWorkerQueue([NotNull] Action processItemAction, bool runProcess, long betweenTime) { if (processItemAction == null) throw new ArgumentNullException("processItemAction"); + if (betweenTime < 0) + throw new ArgumentOutOfRangeException("betweenTime", "Between time can't be negative"); + m_Queue = new PriorityQueue(); m_QueueSection = new SafeCriticalSection(); - m_ProcessSection = new SafeCriticalSection(); - + m_FlushEvent = new IcdManualResetEvent(true); + m_RunProcess = runProcess; m_ProcessAction = processItemAction; + m_BetweenTimeTimer = SafeTimer.Stopped(BetweenTimerCallback); + + BetweenTime = betweenTime; } - #region Queue Methods + #endregion + + #region Methods /// /// Clears the collection. @@ -44,6 +142,68 @@ namespace ICD.Common.Utils m_QueueSection.Execute(() => m_Queue.Clear()); } + /// + /// Blocks until the queue is empty + /// + /// true if the queue empties + [PublicAPI] + public bool WaitForFlush() + { + return m_FlushEvent.WaitOne(); + } + + /// + /// Blocks until the queue is empty, or the timeout is reached + /// + /// Timeout in ms + /// true if the queue empties, false if the timeout is reached + [PublicAPI] + public bool WaitForFlush(int timeout) + { + return m_FlushEvent.WaitOne(timeout); + } + + public void SetRunProcess(bool runProcess) + { + m_QueueSection.Enter(); + try + { + if (m_RunProcess == runProcess) + return; + + m_RunProcess = runProcess; + + if (runProcess) + EnableProcessQueue(); + + } + finally + { + m_QueueSection.Leave(); + } + } + + public void Dispose() + { + m_QueueSection.Enter(); + try + { + SetRunProcess(false); + m_Queue.Clear(); + m_BetweenTimeTimer.Stop(); + m_BetweenTimeTimer.Dispose(); + m_FlushEvent.Dispose(); + } + finally + { + m_QueueSection.Leave(); + } + } + + #endregion + + #region Enqueue Methods + /// /// Adds the item to the end of the queue. /// @@ -51,7 +211,7 @@ namespace ICD.Common.Utils [PublicAPI] public void Enqueue([CanBeNull] T item) { - Enqueue(() => m_Queue.Enqueue(item)); + Enqueue(item, () => m_Queue.Enqueue(item)); } /// @@ -63,7 +223,7 @@ namespace ICD.Common.Utils [PublicAPI] public void Enqueue([CanBeNull] T item, int priority) { - Enqueue(() => m_Queue.Enqueue(item, priority)); + Enqueue(item, () => m_Queue.Enqueue(item, priority)); } /// @@ -75,7 +235,7 @@ namespace ICD.Common.Utils [PublicAPI] public void Enqueue([CanBeNull] T item, int priority, int position) { - Enqueue(() => m_Queue.Enqueue(item, priority, position)); + Enqueue(item, () => m_Queue.Enqueue(item, priority, position)); } /// @@ -85,7 +245,7 @@ namespace ICD.Common.Utils [PublicAPI] public void EnqueueFirst([CanBeNull] T item) { - Enqueue(() => m_Queue.EnqueueFirst(item)); + Enqueue(item, () => m_Queue.EnqueueFirst(item)); } /// @@ -98,7 +258,7 @@ namespace ICD.Common.Utils [PublicAPI] public void EnqueueRemove([CanBeNull] T item, [NotNull] Func remove) { - Enqueue(() => m_Queue.EnqueueRemove(item, remove)); + Enqueue(item, () => m_Queue.EnqueueRemove(item, remove)); } /// @@ -112,7 +272,7 @@ namespace ICD.Common.Utils [PublicAPI] public void EnqueueRemove([CanBeNull] T item, [NotNull] Func remove, int priority) { - Enqueue(() => m_Queue.EnqueueRemove(item, remove, priority)); + Enqueue(item, () => m_Queue.EnqueueRemove(item, remove, priority)); } /// @@ -127,34 +287,168 @@ namespace ICD.Common.Utils [PublicAPI] public void EnqueueRemove([CanBeNull] T item, [NotNull] Func remove, int priority, bool deDuplicateToEndOfQueue) { - Enqueue(() => m_Queue.EnqueueRemove(item, remove, priority, deDuplicateToEndOfQueue)); + Enqueue(item, () => m_Queue.EnqueueRemove(item, remove, priority, deDuplicateToEndOfQueue)); } #endregion #region Private Methods - private void Enqueue(Action enqueueAction) + /// + /// Enqueues an item using the given enqueueAction + /// Starts processing the queue if it's not already processing + /// + /// + /// + private void Enqueue(T item, Action enqueueAction) { - m_QueueSection.Execute(enqueueAction); - ThreadingUtils.SafeInvoke(ProcessQueue); - } + bool startWorkerThread; + T nextItem = default(T); - private void ProcessQueue() - { - if (!m_ProcessSection.TryEnter()) - return; + m_QueueSection.Enter(); try { - T item = default(T); - while (m_QueueSection.Execute(() => m_Queue.TryDequeue(out item))) - m_ProcessAction(item); + // Reset the flush event, to block the flush thread while something is in the queue/being processed + m_FlushEvent.Reset(); + + // We'll start a worker thread if the process isn't running and should be + startWorkerThread = !m_ProcessRunning && RunProcess; + + if (startWorkerThread) + m_ProcessRunning = true; + + + if (m_Queue.Count == 0 && startWorkerThread) + { + // If the queue is empty and we're starting the worker thread + // we won't bother adding an item just to dequeue it again + nextItem = item; + } + else if (startWorkerThread) + { + // If the queue isn't empty and we are starting the worker thread + // enqueue the item and dequeue the next item. + enqueueAction(); + nextItem = m_Queue.Dequeue(); + } + else + { + // If we're not starting the worker thread, just enqueue the item + enqueueAction(); + } } finally { - m_ProcessSection.Leave(); + m_QueueSection.Leave(); } + + if (startWorkerThread) + ProcessQueue(nextItem); + } + + /// + /// Starts processing the queue by starting a thread + /// When running this, m_ProcessRunning MUST be set to true already, + /// and RunProcess should be checked before this + /// + /// + private void ProcessQueue(T item) + { + ThreadingUtils.SafeInvoke(() => ProcessQueueThread(item)); + } + + /// + /// Processes the item, meant to be run in it's own thread + /// When running this, m_ProcessRunning MUST be set to true already, + /// and RunProcess should be checked before this + /// + /// + private void ProcessQueueThread(T item) + { + + while (true) + { + // Run the process action + m_ProcessAction(item); + + m_QueueSection.Enter(); + try + { + bool runProcess = RunProcess; + bool hasItem = m_Queue.Count > 0; + long betweenTime = BetweenTime; + + // Stop processing and return if we don't have another item, or we aren't to keep running the process + if (!runProcess || !hasItem) + { + m_ProcessRunning = false; + if (!hasItem) + m_FlushEvent.Set(); + return; + } + + // If a between time is set, start the timer (and leave m_ProcessRunning set) + if (betweenTime > 0) + { + m_BetweenTimeTimer.Reset(betweenTime); + return; + } + + item = m_Queue.Dequeue(); + } + finally + { + m_QueueSection.Leave(); + } + } + } + + /// + /// Starts processing an item if we should be + /// Run by RunProcess being set to true + /// + private void EnableProcessQueue() + { + T item; + m_QueueSection.Enter(); + try + { + if (!RunProcess || m_ProcessRunning || !m_Queue.TryDequeue(out item)) + return; + + m_ProcessRunning = true; + } + finally + { + m_QueueSection.Leave(); + } + + ProcessQueue(item); + } + + private void BetweenTimerCallback() + { + T item; + + m_QueueSection.Enter(); + try + { + // Check to make sure RunProcess is still true + // Try to dequeue item + if (!RunProcess || !m_Queue.TryDequeue(out item)) + { + m_ProcessRunning = false; + return; + } + } + finally + { + m_QueueSection.Leave(); + } + + // Process the dequeued item + ProcessQueueThread(item); } #endregion From 84b5b636f63d30a6a8177af45f71f1af35d1f0db Mon Sep 17 00:00:00 2001 From: Drew Tingen Date: Mon, 27 Sep 2021 12:20:59 -0400 Subject: [PATCH 4/7] remove: Removing RateLimitedEventQueue - Use ThreadedWorkerQueue instead --- .../Collections/RateLimitedEventQueueTest.cs | 100 --------- .../Collections/RateLimitedEventQueue.cs | 204 ------------------ .../ICD.Common.Utils_SimplSharp.csproj | 1 - 3 files changed, 305 deletions(-) delete mode 100644 ICD.Common.Utils.Tests/Collections/RateLimitedEventQueueTest.cs delete mode 100644 ICD.Common.Utils/Collections/RateLimitedEventQueue.cs diff --git a/ICD.Common.Utils.Tests/Collections/RateLimitedEventQueueTest.cs b/ICD.Common.Utils.Tests/Collections/RateLimitedEventQueueTest.cs deleted file mode 100644 index 62c20b2..0000000 --- a/ICD.Common.Utils.Tests/Collections/RateLimitedEventQueueTest.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using ICD.Common.Utils.Collections; -using ICD.Common.Utils.EventArguments; -using NUnit.Framework; - -namespace ICD.Common.Utils.Tests.Collections -{ - [TestFixture] - public sealed class RateLimitedEventQueueTest - { - [Test] - public void ItemDequeuedFeedbackTest() - { - List> callbacks = new List>(); - - using (RateLimitedEventQueue queue = new RateLimitedEventQueue { BetweenMilliseconds = 1000 }) - { - queue.OnItemDequeued += (sender, args) => callbacks.Add(args); - - queue.Enqueue(10); - queue.Enqueue(20); - queue.Enqueue(30); - - ThreadingUtils.Sleep(100); - - Assert.AreEqual(1, callbacks.Count, "Initial enqueue did not trigger a dequeue"); - - queue.OnItemDequeued += (sender, args) => { ThreadingUtils.Sleep(1000); }; - ThreadingUtils.Sleep(1000); - - Assert.AreEqual(2, callbacks.Count, "Second enqueue did not dequeue"); - - ThreadingUtils.Sleep(1000); - - Assert.AreEqual(2, callbacks.Count, "Third enqueue did not wait for process to complete"); - - ThreadingUtils.Sleep(1000); - - Assert.AreEqual(3, callbacks.Count); - } - } - - #region Properties - - [TestCase(1000)] - public void BetweenMillisecondsTest(long milliseconds) - { - using (RateLimitedEventQueue queue = new RateLimitedEventQueue { BetweenMilliseconds = milliseconds }) - Assert.AreEqual(milliseconds, queue.BetweenMilliseconds); - } - - [Test] - public void CountTest() - { - using (RateLimitedEventQueue queue = new RateLimitedEventQueue { BetweenMilliseconds = 100 * 1000 }) - { - queue.Enqueue(1); - queue.Enqueue(1); - queue.Enqueue(1); - - Assert.AreEqual(3, queue.Count); - } - } - - #endregion - - #region Methods - - [Test] - public void EnqueueTest() - { - using (RateLimitedEventQueue queue = new RateLimitedEventQueue { BetweenMilliseconds = 100 * 1000 }) - { - queue.Enqueue(10); - queue.Enqueue(20); - queue.Enqueue(30); - - Assert.True(queue.SequenceEqual(new[] { 10, 20, 30 })); - } - } - - [Test] - public void ClearTest() - { - using (RateLimitedEventQueue queue = new RateLimitedEventQueue { BetweenMilliseconds = 100 * 1000 }) - { - queue.Enqueue(1); - queue.Enqueue(1); - queue.Enqueue(1); - - queue.Clear(); - - Assert.AreEqual(0, queue.Count); - } - } - - #endregion - } -} diff --git a/ICD.Common.Utils/Collections/RateLimitedEventQueue.cs b/ICD.Common.Utils/Collections/RateLimitedEventQueue.cs deleted file mode 100644 index 15f7976..0000000 --- a/ICD.Common.Utils/Collections/RateLimitedEventQueue.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using ICD.Common.Properties; -using ICD.Common.Utils.EventArguments; -using ICD.Common.Utils.Extensions; -using ICD.Common.Utils.Timers; -#if !SIMPLSHARP -using System.Diagnostics; -#endif - -namespace ICD.Common.Utils.Collections -{ - /// - /// RateLimitedEventQueue provides features for enqueing items to be raised via an event at a controlled interval. - /// -#if !SIMPLSHARP - [DebuggerDisplay("Count = {Count}")] -#endif - public sealed class RateLimitedEventQueue : IEnumerable, ICollection, IDisposable - { - /// - /// Raised to handle to the next item in the queue. - /// - public event EventHandler> OnItemDequeued; - - private readonly SafeTimer m_DequeueTimer; - private readonly Queue m_Queue; - private readonly SafeCriticalSection m_QueueSection; - - #region Properties - - /// - /// Gets/sets the time between dequeues in milliseconds. - /// - public long BetweenMilliseconds { get; set; } - - public int Count { get { return m_QueueSection.Execute(() => m_Queue.Count); } } - - bool ICollection.IsSynchronized { get { return true; } } - - [NotNull] - object ICollection.SyncRoot { get { return this; } } - - #endregion - - /// - /// Constructor. - /// - public RateLimitedEventQueue() - { - m_Queue = new Queue(); - m_QueueSection = new SafeCriticalSection(); - - m_DequeueTimer = SafeTimer.Stopped(DequeueTimerCallback); - } - - #region Methods - - /// - /// Release resources. - /// - public void Dispose() - { - OnItemDequeued = null; - - m_QueueSection.Enter(); - - try - { - m_DequeueTimer.Dispose(); - } - finally - { - m_QueueSection.Leave(); - } - } - - /// - /// Enqueues the given item. - /// - /// - public void Enqueue([CanBeNull] T item) - { - m_QueueSection.Enter(); - - try - { - m_Queue.Enqueue(item); - - if (m_Queue.Count == 1) - SendNext(); - } - finally - { - m_QueueSection.Leave(); - } - } - - /// - /// Clears the queued items. - /// - public void Clear() - { - m_QueueSection.Enter(); - - try - { - m_DequeueTimer.Stop(); - m_Queue.Clear(); - } - finally - { - m_QueueSection.Leave(); - } - } - - #endregion - - #region Private Methods - - /// - /// Sends the next pulse in the queue. - /// - private void SendNext() - { - if (!m_QueueSection.TryEnter()) - return; - - try - { - if (m_Queue.Count == 0) - return; - - T item = m_Queue.Peek(); - - OnItemDequeued.Raise(this, new GenericEventArgs(item)); - - m_DequeueTimer.Reset(BetweenMilliseconds); - } - finally - { - m_QueueSection.Leave(); - } - } - - /// - /// Called when the dequeue timer elapses. - /// - private void DequeueTimerCallback() - { - m_QueueSection.Enter(); - - try - { - m_Queue.Dequeue(); - SendNext(); - } - finally - { - m_QueueSection.Leave(); - } - } - - #endregion - - #region IEnumerable/ICollection - - [NotNull] - public IEnumerator GetEnumerator() - { - return m_QueueSection.Execute(() => m_Queue.ToList(m_Queue.Count).GetEnumerator()); - } - - [NotNull] - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - void ICollection.CopyTo([NotNull] Array array, int index) - { - if (array == null) - throw new ArgumentNullException("array"); - - m_QueueSection.Enter(); - - try - { - foreach (T item in this) - { - array.SetValue(item, index); - index++; - } - } - finally - { - m_QueueSection.Leave(); - } - } - - #endregion - } -} diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index 75c17bb..b7d113c 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -84,7 +84,6 @@ - From 033008616f636da87f783b06534312f740fcc96b Mon Sep 17 00:00:00 2001 From: Drew Tingen Date: Mon, 27 Sep 2021 12:49:55 -0400 Subject: [PATCH 5/7] remove: SafeCriticalSection - remove TryEnter method --- .../SafeCriticalSectionTest.cs | 27 +------------------ .../SafeCriticalSection.SimplSharp.cs | 25 ----------------- .../SafeCriticalSection.Standard.cs | 11 -------- 3 files changed, 1 insertion(+), 62 deletions(-) diff --git a/ICD.Common.Utils.Tests/SafeCriticalSectionTest.cs b/ICD.Common.Utils.Tests/SafeCriticalSectionTest.cs index 193d406..a67cc6f 100644 --- a/ICD.Common.Utils.Tests/SafeCriticalSectionTest.cs +++ b/ICD.Common.Utils.Tests/SafeCriticalSectionTest.cs @@ -37,30 +37,5 @@ namespace ICD.Common.Utils.Tests Assert.Inconclusive(); } - - [Test] - public void TryEnterTest() - { - int result = 0; - - SafeCriticalSection section = new SafeCriticalSection(); - section.Enter(); - - // ReSharper disable once NotAccessedVariable - ThreadingUtils.SafeInvoke(() => { result = section.TryEnter() ? 0 : 1; }); - - Assert.IsTrue(ThreadingUtils.Wait(() => result == 1, 1000)); - - section.Leave(); - - // ReSharper disable once RedundantAssignment - ThreadingUtils.SafeInvoke(() => - { - result = section.TryEnter() ? 2 : 0; - section.Leave(); - }); - - Assert.IsTrue(ThreadingUtils.Wait(() => result == 2, 1000)); - } - } + } } diff --git a/ICD.Common.Utils/SafeCriticalSection.SimplSharp.cs b/ICD.Common.Utils/SafeCriticalSection.SimplSharp.cs index dae97c1..443a75b 100644 --- a/ICD.Common.Utils/SafeCriticalSection.SimplSharp.cs +++ b/ICD.Common.Utils/SafeCriticalSection.SimplSharp.cs @@ -90,31 +90,6 @@ namespace ICD.Common.Utils } } - /// - /// Attempt to enter the critical section without blocking. - /// - /// - /// True, calling thread has ownership of the critical section; otherwise, false. - /// - public bool TryEnter() - { - if (m_CriticalSection == null) - return false; - - try - { -#if DEBUG - return m_CriticalSection.WaitForMutex(0); -#else - return m_CriticalSection.TryEnter(); -#endif - } - catch (ObjectDisposedException) - { - return false; - } - } - #endregion } } diff --git a/ICD.Common.Utils/SafeCriticalSection.Standard.cs b/ICD.Common.Utils/SafeCriticalSection.Standard.cs index 7b460a6..8b8c56c 100644 --- a/ICD.Common.Utils/SafeCriticalSection.Standard.cs +++ b/ICD.Common.Utils/SafeCriticalSection.Standard.cs @@ -24,17 +24,6 @@ namespace ICD.Common.Utils Monitor.Exit(this); } - /// - /// Attempt to enter the critical section without blocking. - /// - /// - /// True, calling thread has ownership of the critical section; otherwise, false. - /// - public bool TryEnter() - { - return Monitor.TryEnter(this); - } - #endregion } } From 79c1c60d79afbe8cdb16db5979aaa74fd17d6080 Mon Sep 17 00:00:00 2001 From: Drew Tingen Date: Wed, 29 Sep 2021 15:06:56 -0400 Subject: [PATCH 6/7] chore: changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb86ad6..9a7408f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + - Added IcdAutoResetEvent and IcdManaualResetEvent + +### Changed + - EnumUtils - Fixed bug where not all values were returned for GetValueExceptNone + - ThreadedWorkerQueue - Added BetweenTime property, to wait between process callbacks + - ThreadedWorkerQueue - Added RunProcess option to stop queue from processing items + - ThreadedWorkerQueue - Added WaitForFlush events to wait until the queue is empty + - ThreadedWorkerQueue - Added count property + - ThreadedWorkerQueue - Now implements IDisposable + +### Removed + - Removed RateLimitedEventQueue and tests - features added to ThreadedWorkerQueue + - Removed TryEnter from SafeCriticalSection - incorrect behavior on Crestron systems ## [15.2.0] - 2021-08-18 ### Added From 7d5ec0e636cc044c17f33511d618ef5fa27028fd Mon Sep 17 00:00:00 2001 From: Drew Tingen Date: Mon, 4 Oct 2021 12:21:41 -0400 Subject: [PATCH 7/7] chore: Update changelog, increment assembly major version --- CHANGELOG.md | 2 ++ ICD.Common.Utils/Properties/AssemblyInfo.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a7408f..66cb95c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [16.0.0] 2021-10-04 ### Added - Added IcdAutoResetEvent and IcdManaualResetEvent diff --git a/ICD.Common.Utils/Properties/AssemblyInfo.cs b/ICD.Common.Utils/Properties/AssemblyInfo.cs index f1046a1..79181bb 100644 --- a/ICD.Common.Utils/Properties/AssemblyInfo.cs +++ b/ICD.Common.Utils/Properties/AssemblyInfo.cs @@ -4,4 +4,4 @@ using System.Reflection; [assembly: AssemblyCompany("ICD Systems")] [assembly: AssemblyProduct("ICD.Common.Utils")] [assembly: AssemblyCopyright("Copyright © ICD Systems 2021")] -[assembly: AssemblyVersion("15.2.0.0")] +[assembly: AssemblyVersion("16.0.0.0")]