Replace the Async coordination primitives & replace the MqttTopicFilterComparer to improve the performancerelease/3.x.x
@@ -1,26 +1,108 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace MQTTnet.Internal | namespace MQTTnet.Internal | ||||
{ | { | ||||
public sealed class AsyncAutoResetEvent : IDisposable | |||||
// Inspired from Stephen Toub (https://blogs.msdn.microsoft.com/pfxteam/2012/02/11/building-async-coordination-primitives-part-2-asyncautoresetevent/) and Chris Gillum (https://stackoverflow.com/a/43012490) | |||||
public class AsyncAutoResetEvent | |||||
{ | { | ||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0, 1); | |||||
private readonly LinkedList<TaskCompletionSource<bool>> _waiters = new LinkedList<TaskCompletionSource<bool>>(); | |||||
private bool _isSignaled; | |||||
public Task WaitOneAsync(CancellationToken cancellationToken) | |||||
public AsyncAutoResetEvent() : this(false) | |||||
{ } | |||||
public AsyncAutoResetEvent(bool signaled) | |||||
{ | { | ||||
return _semaphore.WaitAsync(cancellationToken); | |||||
_isSignaled = signaled; | |||||
} | } | ||||
public void Set() | |||||
public Task<bool> WaitOneAsync() | |||||
{ | |||||
return WaitOneAsync(CancellationToken.None); | |||||
} | |||||
public Task<bool> WaitOneAsync(TimeSpan timeout) | |||||
{ | { | ||||
_semaphore.Release(); | |||||
return WaitOneAsync(timeout, CancellationToken.None); | |||||
} | } | ||||
public void Dispose() | |||||
public Task<bool> WaitOneAsync(CancellationToken cancellationToken) | |||||
{ | { | ||||
_semaphore?.Dispose(); | |||||
return WaitOneAsync(Timeout.InfiniteTimeSpan, cancellationToken); | |||||
} | |||||
public async Task<bool> WaitOneAsync(TimeSpan timeout, CancellationToken cancellationToken) | |||||
{ | |||||
cancellationToken.ThrowIfCancellationRequested(); | |||||
TaskCompletionSource<bool> tcs; | |||||
lock (_waiters) | |||||
{ | |||||
if (_isSignaled) | |||||
{ | |||||
_isSignaled = false; | |||||
return true; | |||||
} | |||||
else if (timeout == TimeSpan.Zero) | |||||
{ | |||||
return _isSignaled; | |||||
} | |||||
else | |||||
{ | |||||
tcs = new TaskCompletionSource<bool>(); | |||||
_waiters.AddLast(tcs); | |||||
} | |||||
} | |||||
Task winner = await Task.WhenAny(tcs.Task, Task.Delay(timeout, cancellationToken)).ConfigureAwait(false); | |||||
if (winner == tcs.Task) | |||||
{ | |||||
// The task was signaled. | |||||
return true; | |||||
} | |||||
else | |||||
{ | |||||
// We timed-out; remove our reference to the task. | |||||
// This is an O(n) operation since waiters is a LinkedList<T>. | |||||
lock (_waiters) | |||||
{ | |||||
_waiters.Remove(tcs); | |||||
if (winner.Status == TaskStatus.Canceled) | |||||
{ | |||||
throw new OperationCanceledException(cancellationToken); | |||||
} | |||||
else | |||||
{ | |||||
throw new TimeoutException(); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
public void Set() | |||||
{ | |||||
TaskCompletionSource<bool> toRelease = null; | |||||
lock (_waiters) | |||||
{ | |||||
if (_waiters.Count > 0) | |||||
{ | |||||
// Signal the first task in the waiters list. | |||||
toRelease = _waiters.First.Value; | |||||
_waiters.RemoveFirst(); | |||||
} | |||||
else if (!_isSignaled) | |||||
{ | |||||
// No tasks are pending | |||||
_isSignaled = true; | |||||
} | |||||
} | |||||
toRelease?.SetResult(true); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -4,23 +4,45 @@ using System.Threading.Tasks; | |||||
namespace MQTTnet.Internal | namespace MQTTnet.Internal | ||||
{ | { | ||||
// From Stephen Toub (https://blogs.msdn.microsoft.com/pfxteam/2012/02/12/building-async-coordination-primitives-part-6-asynclock/) | |||||
public sealed class AsyncLock : IDisposable | public sealed class AsyncLock : IDisposable | ||||
{ | { | ||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); | private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); | ||||
private readonly Task<IDisposable> _releaser; | |||||
public Task EnterAsync(CancellationToken cancellationToken) | |||||
public AsyncLock() | |||||
{ | { | ||||
return _semaphore.WaitAsync(cancellationToken); | |||||
_releaser = Task.FromResult((IDisposable)new Releaser(this)); | |||||
} | } | ||||
public void Exit() | |||||
public Task<IDisposable> LockAsync(CancellationToken cancellationToken) | |||||
{ | { | ||||
_semaphore.Release(); | |||||
Task wait = _semaphore.WaitAsync(cancellationToken); | |||||
return wait.IsCompleted ? | |||||
_releaser : | |||||
wait.ContinueWith((_, state) => (IDisposable)state, | |||||
_releaser.Result, cancellationToken, | |||||
TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); | |||||
} | } | ||||
public void Dispose() | public void Dispose() | ||||
{ | { | ||||
_semaphore?.Dispose(); | _semaphore?.Dispose(); | ||||
} | } | ||||
private sealed class Releaser : IDisposable | |||||
{ | |||||
private readonly AsyncLock _toRelease; | |||||
internal Releaser(AsyncLock toRelease) | |||||
{ | |||||
_toRelease = toRelease; | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
_toRelease._semaphore.Release(); | |||||
} | |||||
} | |||||
} | } | ||||
} | } |
@@ -75,7 +75,7 @@ namespace MQTTnet.Server | |||||
public void Dispose() | public void Dispose() | ||||
{ | { | ||||
_queueAutoResetEvent?.Dispose(); | |||||
} | } | ||||
private async Task SendQueuedPacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | private async Task SendQueuedPacketsAsync(IMqttChannelAdapter adapter, CancellationToken cancellationToken) | ||||
@@ -136,9 +136,9 @@ namespace MQTTnet.Server | |||||
return Task.FromResult((IList<IMqttClientSessionStatus>)result); | return Task.FromResult((IList<IMqttClientSessionStatus>)result); | ||||
} | } | ||||
public void StartDispatchApplicationMessage(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage) | |||||
public Task StartDispatchApplicationMessage(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage) | |||||
{ | { | ||||
Task.Run(() => DispatchApplicationMessageAsync(senderClientSession, applicationMessage)); | |||||
return DispatchApplicationMessageAsync(senderClientSession, applicationMessage); | |||||
} | } | ||||
public Task SubscribeAsync(string clientId, IList<TopicFilter> topicFilters) | public Task SubscribeAsync(string clientId, IList<TopicFilter> topicFilters) | ||||
@@ -197,8 +197,7 @@ namespace MQTTnet.Server | |||||
private async Task<GetOrCreateClientSessionResult> PrepareClientSessionAsync(MqttConnectPacket connectPacket) | private async Task<GetOrCreateClientSessionResult> PrepareClientSessionAsync(MqttConnectPacket connectPacket) | ||||
{ | { | ||||
await _sessionPreparationLock.EnterAsync(CancellationToken.None).ConfigureAwait(false); | |||||
try | |||||
using (await _sessionPreparationLock.LockAsync(CancellationToken.None).ConfigureAwait(false)) | |||||
{ | { | ||||
var isSessionPresent = _sessions.TryGetValue(connectPacket.ClientId, out var clientSession); | var isSessionPresent = _sessions.TryGetValue(connectPacket.ClientId, out var clientSession); | ||||
if (isSessionPresent) | if (isSessionPresent) | ||||
@@ -232,10 +231,6 @@ namespace MQTTnet.Server | |||||
return new GetOrCreateClientSessionResult { IsExistingSession = isExistingSession, Session = clientSession }; | return new GetOrCreateClientSessionResult { IsExistingSession = isExistingSession, Session = clientSession }; | ||||
} | } | ||||
finally | |||||
{ | |||||
_sessionPreparationLock.Exit(); | |||||
} | |||||
} | } | ||||
private async Task DispatchApplicationMessageAsync(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage) | private async Task DispatchApplicationMessageAsync(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage) | ||||
@@ -82,7 +82,7 @@ namespace MQTTnet.Server | |||||
{ | { | ||||
var saveIsRequired = false; | var saveIsRequired = false; | ||||
if (applicationMessage.Payload?.Length > 0) | |||||
if (applicationMessage.Payload?.Length == 0) | |||||
{ | { | ||||
saveIsRequired = _messages.TryRemove(applicationMessage.Topic, out _); | saveIsRequired = _messages.TryRemove(applicationMessage.Topic, out _); | ||||
_logger.Info("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); | _logger.Info("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); | ||||
@@ -93,7 +93,7 @@ namespace MQTTnet.Server | |||||
} | } | ||||
_logger.Info("Started."); | _logger.Info("Started."); | ||||
Started?.Invoke(this, new MqttServerStartedEventArgs()); | |||||
Started?.Invoke(this, EventArgs.Empty); | |||||
} | } | ||||
public async Task StopAsync() | public async Task StopAsync() | ||||
@@ -117,6 +117,7 @@ namespace MQTTnet.Server | |||||
await _clientSessionsManager.StopAsync().ConfigureAwait(false); | await _clientSessionsManager.StopAsync().ConfigureAwait(false); | ||||
_logger.Info("Stopped."); | _logger.Info("Stopped."); | ||||
Stopped?.Invoke(this, EventArgs.Empty); | |||||
} | } | ||||
finally | finally | ||||
{ | { | ||||
@@ -4,46 +4,116 @@ namespace MQTTnet.Server | |||||
{ | { | ||||
public static class MqttTopicFilterComparer | public static class MqttTopicFilterComparer | ||||
{ | { | ||||
private static readonly char[] TopicLevelSeparator = { '/' }; | |||||
private const char LEVEL_SEPARATOR = '/'; | |||||
private const char WILDCARD_MULTI_LEVEL = '#'; | |||||
private const char WILDCARD_SINGLE_LEVEL = '+'; | |||||
public static bool IsMatch(string topic, string filter) | public static bool IsMatch(string topic, string filter) | ||||
{ | { | ||||
if (topic == null) throw new ArgumentNullException(nameof(topic)); | |||||
if (filter == null) throw new ArgumentNullException(nameof(filter)); | |||||
if (string.IsNullOrEmpty(topic)) throw new ArgumentNullException(nameof(topic)); | |||||
if (string.IsNullOrEmpty(filter)) throw new ArgumentNullException(nameof(filter)); | |||||
if (string.Equals(topic, filter, StringComparison.Ordinal)) | |||||
{ | |||||
return true; | |||||
} | |||||
var fragmentsTopic = topic.Split(TopicLevelSeparator, StringSplitOptions.None); | |||||
var fragmentsFilter = filter.Split(TopicLevelSeparator, StringSplitOptions.None); | |||||
int spos = 0; | |||||
int slen = filter.Length; | |||||
int tpos = 0; | |||||
int tlen = topic.Length; | |||||
// # > In either case it MUST be the last character specified in the Topic Filter [MQTT-4.7.1-2]. | |||||
for (var i = 0; i < fragmentsFilter.Length; i++) | |||||
while (spos < slen && tpos < tlen) | |||||
{ | { | ||||
if (fragmentsFilter[i] == "+") | |||||
{ | |||||
continue; | |||||
} | |||||
if (fragmentsFilter[i] == "#") | |||||
if (filter[spos] == topic[tpos]) | |||||
{ | { | ||||
return true; | |||||
if (tpos == tlen - 1) | |||||
{ | |||||
/* Check for e.g. foo matching foo/# */ | |||||
if (spos == slen - 3 | |||||
&& filter[spos + 1] == LEVEL_SEPARATOR | |||||
&& filter[spos + 2] == WILDCARD_MULTI_LEVEL) | |||||
{ | |||||
return true; | |||||
} | |||||
} | |||||
spos++; | |||||
tpos++; | |||||
if (spos == slen && tpos == tlen) | |||||
{ | |||||
return true; | |||||
} | |||||
else if (tpos == tlen && spos == slen - 1 && filter[spos] == WILDCARD_SINGLE_LEVEL) | |||||
{ | |||||
if (spos > 0 && filter[spos - 1] != LEVEL_SEPARATOR) | |||||
{ | |||||
// Invalid filter string | |||||
return false; | |||||
} | |||||
spos++; | |||||
return true; | |||||
} | |||||
} | } | ||||
if (i >= fragmentsTopic.Length) | |||||
{ | |||||
return false; | |||||
} | |||||
if (!string.Equals(fragmentsFilter[i], fragmentsTopic[i], StringComparison.Ordinal)) | |||||
else | |||||
{ | { | ||||
return false; | |||||
if (filter[spos] == WILDCARD_SINGLE_LEVEL) | |||||
{ | |||||
/* Check for bad "+foo" or "a/+foo" subscription */ | |||||
if (spos > 0 && filter[spos - 1] != LEVEL_SEPARATOR) | |||||
{ | |||||
// Invalid filter string | |||||
return false; | |||||
} | |||||
/* Check for bad "foo+" or "foo+/a" subscription */ | |||||
if (spos < slen - 1 && filter[spos + 1] != LEVEL_SEPARATOR) | |||||
{ | |||||
// Invalid filter string | |||||
return false; | |||||
} | |||||
spos++; | |||||
while (tpos < tlen && topic[tpos] != LEVEL_SEPARATOR) | |||||
{ | |||||
tpos++; | |||||
} | |||||
if (tpos == tlen && spos == slen) | |||||
{ | |||||
return true; | |||||
} | |||||
} | |||||
else if (filter[spos] == WILDCARD_MULTI_LEVEL) | |||||
{ | |||||
if (spos > 0 && filter[spos - 1] != LEVEL_SEPARATOR) | |||||
{ | |||||
// Invalid filter string | |||||
return false; | |||||
} | |||||
if (spos + 1 != slen) | |||||
{ | |||||
// Invalid filter string | |||||
return false; | |||||
} | |||||
else | |||||
{ | |||||
return true; | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
/* Check for e.g. foo/bar matching foo/+/# */ | |||||
if (spos > 0 | |||||
&& spos + 2 == slen | |||||
&& tpos == tlen | |||||
&& filter[spos - 1] == WILDCARD_SINGLE_LEVEL | |||||
&& filter[spos] == LEVEL_SEPARATOR | |||||
&& filter[spos + 1] == WILDCARD_MULTI_LEVEL) | |||||
{ | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
} | } | ||||
} | } | ||||
if (tpos < tlen || spos < slen) | |||||
{ | |||||
return false; | |||||
} | |||||
return fragmentsTopic.Length == fragmentsFilter.Length; | |||||
return false; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -147,6 +147,7 @@ | |||||
</Reference> | </Reference> | ||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<Compile Include="TopicFilterComparerBenchmark.cs" /> | |||||
<Compile Include="LoggerBenchmark.cs" /> | <Compile Include="LoggerBenchmark.cs" /> | ||||
<Compile Include="MessageProcessingBenchmark.cs" /> | <Compile Include="MessageProcessingBenchmark.cs" /> | ||||
<Compile Include="Program.cs" /> | <Compile Include="Program.cs" /> | ||||
@@ -12,6 +12,7 @@ namespace MQTTnet.Benchmarks | |||||
Console.WriteLine("1 = MessageProcessingBenchmark"); | Console.WriteLine("1 = MessageProcessingBenchmark"); | ||||
Console.WriteLine("2 = SerializerBenchmark"); | Console.WriteLine("2 = SerializerBenchmark"); | ||||
Console.WriteLine("3 = LoggerBenchmark"); | Console.WriteLine("3 = LoggerBenchmark"); | ||||
Console.WriteLine("4 = TopicFilterComparerBenchmark"); | |||||
var pressedKey = Console.ReadKey(true); | var pressedKey = Console.ReadKey(true); | ||||
switch (pressedKey.KeyChar) | switch (pressedKey.KeyChar) | ||||
@@ -25,6 +26,9 @@ namespace MQTTnet.Benchmarks | |||||
case '3': | case '3': | ||||
BenchmarkRunner.Run<LoggerBenchmark>(); | BenchmarkRunner.Run<LoggerBenchmark>(); | ||||
break; | break; | ||||
case '4': | |||||
BenchmarkRunner.Run<TopicFilterComparerBenchmark>(); | |||||
break; | |||||
} | } | ||||
Console.ReadLine(); | Console.ReadLine(); | ||||
@@ -0,0 +1,91 @@ | |||||
using BenchmarkDotNet.Attributes; | |||||
using BenchmarkDotNet.Attributes.Exporters; | |||||
using BenchmarkDotNet.Attributes.Jobs; | |||||
using MQTTnet.Server; | |||||
using System; | |||||
namespace MQTTnet.Benchmarks | |||||
{ | |||||
[ClrJob] | |||||
[RPlotExporter] | |||||
[MemoryDiagnoser] | |||||
public class TopicFilterComparerBenchmark | |||||
{ | |||||
private static readonly char[] TopicLevelSeparator = { '/' }; | |||||
[GlobalSetup] | |||||
public void Setup() | |||||
{ | |||||
} | |||||
[Benchmark] | |||||
public void MqttTopicFilterComparer_10000_StringSplitMethod() | |||||
{ | |||||
for (var i = 0; i < 10000; i++) | |||||
{ | |||||
LegacyMethodByStringSplit("sport/tennis/player1", "sport/#"); | |||||
LegacyMethodByStringSplit("sport/tennis/player1/ranking", "sport/#/ranking"); | |||||
LegacyMethodByStringSplit("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); | |||||
LegacyMethodByStringSplit("sport/tennis/player1", "sport/tennis/+"); | |||||
LegacyMethodByStringSplit("/finance", "+/+"); | |||||
LegacyMethodByStringSplit("/finance", "/+"); | |||||
LegacyMethodByStringSplit("/finance", "+"); | |||||
} | |||||
} | |||||
[Benchmark] | |||||
public void MqttTopicFilterComparer_10000_LoopMethod() | |||||
{ | |||||
for (var i = 0; i < 10000; i++) | |||||
{ | |||||
MqttTopicFilterComparer.IsMatch("sport/tennis/player1", "sport/#"); | |||||
MqttTopicFilterComparer.IsMatch("sport/tennis/player1/ranking", "sport/#/ranking"); | |||||
MqttTopicFilterComparer.IsMatch("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); | |||||
MqttTopicFilterComparer.IsMatch("sport/tennis/player1", "sport/tennis/+"); | |||||
MqttTopicFilterComparer.IsMatch("/finance", "+/+"); | |||||
MqttTopicFilterComparer.IsMatch("/finance", "/+"); | |||||
MqttTopicFilterComparer.IsMatch("/finance", "+"); | |||||
} | |||||
} | |||||
private static bool LegacyMethodByStringSplit(string topic, string filter) | |||||
{ | |||||
if (topic == null) throw new ArgumentNullException(nameof(topic)); | |||||
if (filter == null) throw new ArgumentNullException(nameof(filter)); | |||||
if (string.Equals(topic, filter, StringComparison.Ordinal)) | |||||
{ | |||||
return true; | |||||
} | |||||
var fragmentsTopic = topic.Split(TopicLevelSeparator, StringSplitOptions.None); | |||||
var fragmentsFilter = filter.Split(TopicLevelSeparator, StringSplitOptions.None); | |||||
// # > In either case it MUST be the last character specified in the Topic Filter [MQTT-4.7.1-2]. | |||||
for (var i = 0; i < fragmentsFilter.Length; i++) | |||||
{ | |||||
if (fragmentsFilter[i] == "+") | |||||
{ | |||||
continue; | |||||
} | |||||
if (fragmentsFilter[i] == "#") | |||||
{ | |||||
return true; | |||||
} | |||||
if (i >= fragmentsTopic.Length) | |||||
{ | |||||
return false; | |||||
} | |||||
if (!string.Equals(fragmentsFilter[i], fragmentsTopic[i], StringComparison.Ordinal)) | |||||
{ | |||||
return false; | |||||
} | |||||
} | |||||
return fragmentsTopic.Length == fragmentsFilter.Length; | |||||
} | |||||
} | |||||
} |
@@ -1,34 +1,210 @@ | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
using Microsoft.VisualStudio.TestTools.UnitTesting; | |||||
using Microsoft.VisualStudio.TestTools.UnitTesting; | |||||
using MQTTnet.Internal; | using MQTTnet.Internal; | ||||
using System; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
namespace MQTTnet.Core.Tests | namespace MQTTnet.Core.Tests | ||||
{ | { | ||||
[TestClass] | [TestClass] | ||||
// Inspired from the vs-threading tests (https://github.com/Microsoft/vs-threading/blob/master/src/Microsoft.VisualStudio.Threading.Tests/AsyncAutoResetEventTests.cs) | |||||
public class AsyncAutoResetEventTests | public class AsyncAutoResetEventTests | ||||
{ | { | ||||
private readonly AsyncAutoResetEvent _aare; | |||||
public AsyncAutoResetEventTests() | |||||
{ | |||||
_aare = new AsyncAutoResetEvent(); | |||||
} | |||||
[TestMethod] | |||||
public async Task SingleThreadedPulse() | |||||
{ | |||||
for (int i = 0; i < 5; i++) | |||||
{ | |||||
var t = _aare.WaitOneAsync(); | |||||
Assert.IsFalse(t.IsCompleted); | |||||
_aare.Set(); | |||||
await t; | |||||
Assert.IsTrue(t.IsCompleted); | |||||
} | |||||
} | |||||
[TestMethod] | |||||
public async Task MultipleSetOnlySignalsOnce() | |||||
{ | |||||
_aare.Set(); | |||||
_aare.Set(); | |||||
await _aare.WaitOneAsync(); | |||||
var t = _aare.WaitOneAsync(); | |||||
Assert.IsFalse(t.IsCompleted); | |||||
await Task.Delay(500); | |||||
Assert.IsFalse(t.IsCompleted); | |||||
_aare.Set(); | |||||
await t; | |||||
Assert.IsTrue(t.IsCompleted); | |||||
} | |||||
[TestMethod] | |||||
public async Task OrderPreservingQueue() | |||||
{ | |||||
var waiters = new Task[5]; | |||||
for (int i = 0; i < waiters.Length; i++) | |||||
{ | |||||
waiters[i] = _aare.WaitOneAsync(); | |||||
} | |||||
for (int i = 0; i < waiters.Length; i++) | |||||
{ | |||||
_aare.Set(); | |||||
await waiters[i].ConfigureAwait(false); | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Verifies that inlining continuations do not have to complete execution before Set() returns. | |||||
/// </summary> | |||||
[TestMethod] | |||||
public async Task SetReturnsBeforeInlinedContinuations() | |||||
{ | |||||
var setReturned = new ManualResetEventSlim(); | |||||
var inlinedContinuation = _aare.WaitOneAsync() | |||||
.ContinueWith(delegate | |||||
{ | |||||
// Arrange to synchronously block the continuation until Set() has returned, | |||||
// which would deadlock if Set does not return until inlined continuations complete. | |||||
Assert.IsTrue(setReturned.Wait(500)); | |||||
}); | |||||
await Task.Delay(100); | |||||
_aare.Set(); | |||||
setReturned.Set(); | |||||
Assert.IsTrue(inlinedContinuation.Wait(500)); | |||||
} | |||||
[TestMethod] | |||||
public void WaitAsync_WithCancellationToken() | |||||
{ | |||||
var cts = new CancellationTokenSource(); | |||||
Task waitTask = _aare.WaitOneAsync(cts.Token); | |||||
Assert.IsFalse(waitTask.IsCompleted); | |||||
// Cancel the request and ensure that it propagates to the task. | |||||
cts.Cancel(); | |||||
try | |||||
{ | |||||
waitTask.GetAwaiter().GetResult(); | |||||
Assert.IsTrue(false, "Task was expected to transition to a canceled state."); | |||||
} | |||||
catch (OperationCanceledException ex) | |||||
{ | |||||
Assert.AreEqual(cts.Token, ex.CancellationToken); | |||||
} | |||||
// Now set the event and verify that a future waiter gets the signal immediately. | |||||
_aare.Set(); | |||||
waitTask = _aare.WaitOneAsync(); | |||||
Assert.AreEqual(TaskStatus.RanToCompletion, waitTask.Status); | |||||
} | |||||
[TestMethod] | |||||
public void WaitAsync_WithCancellationToken_Precanceled() | |||||
{ | |||||
// We construct our own pre-canceled token so that we can do | |||||
// a meaningful identity check later. | |||||
var tokenSource = new CancellationTokenSource(); | |||||
tokenSource.Cancel(); | |||||
var token = tokenSource.Token; | |||||
// Verify that a pre-set signal is not reset by a canceled wait request. | |||||
_aare.Set(); | |||||
try | |||||
{ | |||||
_aare.WaitOneAsync(token).GetAwaiter().GetResult(); | |||||
Assert.IsTrue(false, "Task was expected to transition to a canceled state."); | |||||
} | |||||
catch (OperationCanceledException ex) | |||||
{ | |||||
Assert.AreEqual(token, ex.CancellationToken); | |||||
} | |||||
// Verify that the signal was not acquired. | |||||
Task waitTask = _aare.WaitOneAsync(); | |||||
Assert.AreEqual(TaskStatus.RanToCompletion, waitTask.Status); | |||||
} | |||||
[TestMethod] | |||||
public async Task WaitAsync_WithTimeout() | |||||
{ | |||||
Task waitTask = _aare.WaitOneAsync(TimeSpan.FromMilliseconds(500)); | |||||
Assert.IsFalse(waitTask.IsCompleted); | |||||
// Cancel the request and ensure that it propagates to the task. | |||||
await Task.Delay(1000).ConfigureAwait(false); | |||||
try | |||||
{ | |||||
waitTask.GetAwaiter().GetResult(); | |||||
Assert.IsTrue(false, "Task was expected to transition to a timeout state."); | |||||
} | |||||
catch (TimeoutException) | |||||
{ | |||||
Assert.IsTrue(true); | |||||
} | |||||
// Now set the event and verify that a future waiter gets the signal immediately. | |||||
_aare.Set(); | |||||
waitTask = _aare.WaitOneAsync(TimeSpan.FromMilliseconds(500)); | |||||
Assert.AreEqual(TaskStatus.RanToCompletion, waitTask.Status); | |||||
} | |||||
[TestMethod] | |||||
public void WaitAsync_Canceled_DoesNotInlineContinuations() | |||||
{ | |||||
var cts = new CancellationTokenSource(); | |||||
var task = _aare.WaitOneAsync(cts.Token); | |||||
var completingActionFinished = new ManualResetEventSlim(); | |||||
var continuation = task.ContinueWith( | |||||
_ => Assert.IsTrue(completingActionFinished.Wait(500)), | |||||
CancellationToken.None, | |||||
TaskContinuationOptions.None, | |||||
TaskScheduler.Default); | |||||
cts.Cancel(); | |||||
completingActionFinished.Set(); | |||||
// Rethrow the exception if it turned out it deadlocked. | |||||
continuation.GetAwaiter().GetResult(); | |||||
} | |||||
[TestMethod] | [TestMethod] | ||||
public async Task AsyncAutoResetEvent() | public async Task AsyncAutoResetEvent() | ||||
{ | { | ||||
var aare = new AsyncAutoResetEvent(); | var aare = new AsyncAutoResetEvent(); | ||||
var increment = 0; | |||||
var globalI = 0; | var globalI = 0; | ||||
#pragma warning disable 4014 | #pragma warning disable 4014 | ||||
Task.Run(async () => | Task.Run(async () => | ||||
#pragma warning restore 4014 | #pragma warning restore 4014 | ||||
{ | { | ||||
await aare.WaitOneAsync(CancellationToken.None); | await aare.WaitOneAsync(CancellationToken.None); | ||||
globalI += increment; | |||||
globalI += 1; | |||||
}); | }); | ||||
#pragma warning disable 4014 | |||||
Task.Run(async () => | |||||
#pragma warning restore 4014 | |||||
{ | |||||
await aare.WaitOneAsync(CancellationToken.None); | |||||
globalI += 2; | |||||
}); | |||||
await Task.Delay(500); | |||||
aare.Set(); | |||||
await Task.Delay(500); | await Task.Delay(500); | ||||
increment = 1; | |||||
aare.Set(); | aare.Set(); | ||||
await Task.Delay(100); | await Task.Delay(100); | ||||
Assert.AreEqual(1, globalI); | |||||
Assert.AreEqual(3, globalI); | |||||
} | } | ||||
} | } | ||||
} | |||||
} |
@@ -21,22 +21,17 @@ namespace MQTTnet.Core.Tests | |||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed | #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed | ||||
threads[i] = Task.Run(async () => | threads[i] = Task.Run(async () => | ||||
{ | { | ||||
await @lock.EnterAsync(CancellationToken.None); | |||||
try | |||||
using (var releaser = await @lock.LockAsync(CancellationToken.None)) | |||||
{ | { | ||||
var localI = globalI; | var localI = globalI; | ||||
await Task.Delay(10); // Increase the chance for wrong data. | await Task.Delay(10); // Increase the chance for wrong data. | ||||
localI++; | localI++; | ||||
globalI = localI; | globalI = localI; | ||||
} | } | ||||
finally | |||||
{ | |||||
@lock.Exit(); | |||||
} | |||||
}); | }); | ||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed | #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed | ||||
} | } | ||||
Task.WaitAll(threads); | Task.WaitAll(threads); | ||||
Assert.AreEqual(ThreadsCount, globalI); | Assert.AreEqual(ThreadsCount, globalI); | ||||
} | } | ||||