diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 9f904d4a..f6ec0cc0 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,4 +1,5 @@ * [Core] MQTT Packets being sent over web socket transport are now setting the web socket frame boundaries correctly (#1499). +* [Core] Add support for attaching and detaching events from different threads. * [Client] Keep alive mechanism now uses the configured timeout value from the options (thanks to @Stannieman, #1495). * [Client] The _PingAsync_ will fallback to the timeout specified in the client options when the cancellation token cannot be cancelled. * [Server] A DISCONNECT packet is no longer sent to MQTT clients < 5.0.0 (thanks to @logicaloud, #1506). diff --git a/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs index 3c1fdc90..54442bf6 100644 --- a/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncEvent_Tests.cs @@ -41,6 +41,33 @@ namespace MQTTnet.Tests.Internal Assert.AreEqual(0, _testEventAsyncCount); } + [TestMethod] + public void Has_Handlers() + { + var testClass = new TestClass(); + testClass.TestEventAsync += OnTestEventAsync; + + Assert.AreEqual(true, testClass.HasTestHandlers); + } + + [TestMethod] + public void No_Handlers() + { + var testClass = new TestClass(); + + Assert.AreEqual(false, testClass.HasTestHandlers); + } + + [TestMethod] + public void Remove_Handlers() + { + var testClass = new TestClass(); + testClass.TestEventAsync += OnTestEventAsync; + testClass.TestEventAsync -= OnTestEventAsync; + + Assert.AreEqual(false, testClass.HasTestHandlers); + } + Task OnTestEventAsync(EventArgs arg) { Interlocked.Increment(ref _testEventAsyncCount); @@ -57,6 +84,8 @@ namespace MQTTnet.Tests.Internal remove => _testEvent.RemoveHandler(value); } + public bool HasTestHandlers => _testEvent.HasHandlers; + public Task FireEventAsync(EventArgs eventArgs) { return _testEvent.InvokeAsync(eventArgs); diff --git a/Source/MQTTnet/Internal/AsyncEvent.cs b/Source/MQTTnet/Internal/AsyncEvent.cs index 3edf7a71..cd67b241 100644 --- a/Source/MQTTnet/Internal/AsyncEvent.cs +++ b/Source/MQTTnet/Internal/AsyncEvent.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using MQTTnet.Diagnostics; @@ -13,7 +14,16 @@ namespace MQTTnet.Internal { readonly List> _handlers = new List>(); - public bool HasHandlers => _handlers.Count > 0; + ICollection> _handlersForInvoke; + + public AsyncEvent() + { + _handlersForInvoke = _handlers; + } + + // Track the existence of handlers in a separate field so that checking it all the time will not + // require locking the actual list (_handlers). + public bool HasHandlers { get; private set; } public void AddHandler(Func handler) { @@ -22,7 +32,13 @@ namespace MQTTnet.Internal throw new ArgumentNullException(nameof(handler)); } - _handlers.Add(new AsyncEventInvocator(null, handler)); + lock (_handlers) + { + _handlers.Add(new AsyncEventInvocator(null, handler)); + + HasHandlers = true; + _handlersForInvoke = new List>(_handlers.ToList()); + } } public void AddHandler(Action handler) @@ -32,17 +48,32 @@ namespace MQTTnet.Internal throw new ArgumentNullException(nameof(handler)); } - _handlers.Add(new AsyncEventInvocator(handler, null)); + lock (_handlers) + { + _handlers.Add(new AsyncEventInvocator(handler, null)); + + HasHandlers = true; + _handlersForInvoke = new List>(_handlers.ToList()); + } } public async Task InvokeAsync(TEventArgs eventArgs) { - foreach (var handler in _handlers) + if (!HasHandlers) + { + return; + } + + // Adding or removing handlers will produce a new list instance all the time. + // So locking here is not required since only the reference to an immutable list + // of handlers is used. + var handlers = _handlersForInvoke; + foreach (var handler in handlers) { await handler.InvokeAsync(eventArgs).ConfigureAwait(false); } } - + public void RemoveHandler(Func handler) { if (handler == null) @@ -50,7 +81,13 @@ namespace MQTTnet.Internal throw new ArgumentNullException(nameof(handler)); } - _handlers.RemoveAll(h => h.WrapsHandler(handler)); + lock (_handlers) + { + _handlers.RemoveAll(h => h.WrapsHandler(handler)); + + HasHandlers = _handlers.Count > 0; + _handlersForInvoke = new List>(_handlers.ToList()); + } } public void RemoveHandler(Action handler) @@ -60,7 +97,13 @@ namespace MQTTnet.Internal throw new ArgumentNullException(nameof(handler)); } - _handlers.RemoveAll(h => h.WrapsHandler(handler)); + lock (_handlers) + { + _handlers.RemoveAll(h => h.WrapsHandler(handler)); + + HasHandlers = _handlers.Count > 0; + _handlersForInvoke = new List>(_handlers.ToList()); + } } public async Task TryInvokeAsync(TEventArgs eventArgs, MqttNetSourceLogger logger)