diff --git a/.gitignore b/.gitignore
index f1e3d20..b9df257 100644
--- a/.gitignore
+++ b/.gitignore
@@ -250,3 +250,5 @@ paket-files/
# JetBrains Rider
.idea/
*.sln.iml
+
+NugetApiKey.txt
\ No newline at end of file
diff --git a/AsyncLock.sln b/AsyncLock.sln
deleted file mode 100644
index f8d30ed..0000000
--- a/AsyncLock.sln
+++ /dev/null
@@ -1,31 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29521.150
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncLock", "AsyncLock\AsyncLock.csproj", "{077768A9-D1A4-48BB-8ECF-C66D50E47396}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{7864530D-D038-495F-9283-34A185FBC20F}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {077768A9-D1A4-48BB-8ECF-C66D50E47396}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {077768A9-D1A4-48BB-8ECF-C66D50E47396}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {077768A9-D1A4-48BB-8ECF-C66D50E47396}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {077768A9-D1A4-48BB-8ECF-C66D50E47396}.Release|Any CPU.Build.0 = Release|Any CPU
- {7864530D-D038-495F-9283-34A185FBC20F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7864530D-D038-495F-9283-34A185FBC20F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7864530D-D038-495F-9283-34A185FBC20F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7864530D-D038-495F-9283-34A185FBC20F}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {EE50F46F-060F-49C5-9FD9-2304E0A6C2C6}
- EndGlobalSection
-EndGlobal
diff --git a/AsyncLock.slnx b/AsyncLock.slnx
new file mode 100644
index 0000000..306e16d
--- /dev/null
+++ b/AsyncLock.slnx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/AsyncLock/AsyncLock.cs b/AsyncLock/AsyncLock.cs
index 18d6a12..9089531 100644
--- a/AsyncLock/AsyncLock.cs
+++ b/AsyncLock/AsyncLock.cs
@@ -19,7 +19,7 @@ public class AsyncLock
internal SemaphoreSlim _retry = new SemaphoreSlim(0, 1);
private const long UnlockedId = 0x00; // "owning" task id when unlocked
internal long _owningId = UnlockedId;
- internal int _owningThreadId = (int) UnlockedId;
+ internal int _owningThreadId = (int)UnlockedId;
private static long AsyncStackCounter = 0;
// An AsyncLocal is not really the task-based equivalent to a ThreadLocal, in that
// it does not track the async flow (as the documentation describes) but rather it is
@@ -93,7 +93,8 @@ internal async Task ObtainLockAsync(CancellationToken cancellationT
// In case of zero-timeout, don't even wait for protective lock contention
if (timeout == TimeSpan.Zero)
{
- _parent._reentrancy.Wait(timeout);
+ //BUG? _parent._reentrancy.Wait(timeout);
+ if (!_parent._reentrancy.Wait(timeout)) return null;
if (InnerTryEnter(synchronous: false))
{
// Reset the owning thread id after all await calls have finished, otherwise we
@@ -113,7 +114,8 @@ internal async Task ObtainLockAsync(CancellationToken cancellationT
// We need to wait for someone to leave the lock before trying again.
while (remainder > TimeSpan.Zero)
{
- await _parent._reentrancy.WaitAsync(remainder).ConfigureAwait(false);
+ //BUG? await _parent._reentrancy.WaitAsync(remainder).ConfigureAwait(false);
+ if (!await _parent._reentrancy.WaitAsync(remainder).ConfigureAwait(false)) return null;
if (InnerTryEnter(synchronous: false))
{
// Reset the owning thread id after all await calls have finished, otherwise we
@@ -122,12 +124,14 @@ internal async Task ObtainLockAsync(CancellationToken cancellationT
_parent._reentrancy.Release();
return this;
}
- _parent._reentrancy.Release();
+ //BUG? _parent._reentrancy.Release();
now = DateTimeOffset.UtcNow;
remainder -= now - last;
last = now;
- if (remainder < TimeSpan.Zero)
+ //BUG? if (remainder < TimeSpan.Zero)
+ // <= is correct, cause the loop invariant is remainder > TimeSpan.Zero, and the need to release reentrnacy
+ if (remainder <= TimeSpan.Zero)
{
_parent._reentrancy.Release();
return null;
@@ -202,7 +206,8 @@ internal IDisposable ObtainLock(CancellationToken cancellationToken = default)
// In case of zero-timeout, don't even wait for protective lock contention
if (timeout == TimeSpan.Zero)
{
- _parent._reentrancy.Wait(timeout);
+ //BUG? _parent._reentrancy.Wait(timeout);
+ if (!_parent._reentrancy.Wait(timeout)) return null;
if (InnerTryEnter(synchronous: true))
{
_parent._reentrancy.Release();
@@ -219,7 +224,8 @@ internal IDisposable ObtainLock(CancellationToken cancellationToken = default)
// We need to wait for someone to leave the lock before trying again.
while (remainder > TimeSpan.Zero)
{
- _parent._reentrancy.Wait(remainder);
+ //BUG? _parent._reentrancy.Wait(remainder);
+ if (!_parent._reentrancy.Wait(remainder)) return null;
if (InnerTryEnter(synchronous: true))
{
_parent._reentrancy.Release();
@@ -498,5 +504,34 @@ public bool TryLock(Action callback, TimeSpan timeout)
}
return true;
}
+
+ public Task LockAsync(TimeSpan timeout)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter);
+
+ return @lock.TryObtainLockAsync(timeout)
+ .ContinueWith(state =>
+ {
+ if (state.Exception is AggregateException ex)
+ {
+ ExceptionDispatchInfo.Capture(ex.InnerException!).Throw();
+ }
+ var disposableLock = state.Result;
+ if (disposableLock is null) throw new TimeoutException("LockAsync timed out.");
+ return disposableLock;
+ });
+ }
+
+ public IDisposable Lock(TimeSpan timeout)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ // Increment the async stack counter to prevent a child task from getting
+ // the lock at the same time as a child thread.
+ _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter);
+ var lockDisposable = @lock.TryObtainLock(timeout);
+ if (lockDisposable is null) throw new TimeoutException("TryLock timed out.");
+ return lockDisposable;
+ }
}
}
diff --git a/AsyncLock/AsyncLock.csproj b/AsyncLock/AsyncLock.csproj
index 66bc0f2..60d4c31 100644
--- a/AsyncLock/AsyncLock.csproj
+++ b/AsyncLock/AsyncLock.csproj
@@ -1,7 +1,8 @@
-
+
- netstandard1.3;netstandard2.1
+ netstandard1.3;netstandard2.0;netstandard2.1
+ NeoSmart.AsyncLock
NeoSmart.AsyncLock
NeoSmart.AsyncLock
True
@@ -15,7 +16,7 @@
https://neosmart.net/blog/2017/asynclock-an-asyncawait-friendly-locking-library-for-c-and-net/
https://github.com/neosmart/AsyncLock
git
- asynclock, async await, async, await, lock, synchronization
+ asynclock, async await, async, await, lock, synchronization, mutex, async mutex
3.2: New TryLock() and TryLockAsync() methods, CancellationToken support for synchronous locking routines.
@@ -43,4 +44,9 @@
+
+
+
+
+
diff --git a/AsyncLock/AsyncMutexLock.cs b/AsyncLock/AsyncMutexLock.cs
new file mode 100644
index 0000000..75409bf
--- /dev/null
+++ b/AsyncLock/AsyncMutexLock.cs
@@ -0,0 +1,1012 @@
+#if !NETSTANDARD1_3
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Runtime.ExceptionServices;
+using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace NeoSmart.AsyncLock;
+
+public enum MutexScope { Machine, User }
+
+public class AsyncMutexLock: IDisposable
+{
+ private SemaphoreSlim _reentrancy = new SemaphoreSlim(1, 1);
+ private int _reentrances = 0;
+ // We are using this SemaphoreSlim like a posix condition variable.
+ // We only want to wake waiters, one or more of whom will try to obtain
+ // a different lock to do their thing. So long as we can guarantee no
+ // wakes are missed, the number of awakees is not important.
+ // Ideally, this would be "friend" for access only from InnerLock, but
+ // whatever.
+ internal SemaphoreSlim _retry = new SemaphoreSlim(0, 1);
+ private const long UnlockedId = 0x00; // "owning" task id when unlocked
+ internal long _owningId = UnlockedId;
+ internal int _owningThreadId = (int)UnlockedId;
+ private static long AsyncStackCounter = 0;
+ public string FileName;
+ // An AsyncLocal is not really the task-based equivalent to a ThreadLocal, in that
+ // it does not track the async flow (as the documentation describes) but rather it is
+ // associated with a stack snapshot. Mutation of the AsyncLocal in an await call does
+ // not change the value observed by the parent when the call returns, so if you want to
+ // use it as a persistent async flow identifier, the value needs to be set at the outer-
+ // most level and never touched internally.
+ private static readonly AsyncLocal _asyncId = new AsyncLocal();
+ private static long AsyncId => _asyncId.Value;
+
+#if NETSTANDARD1_3
+ private static int ThreadCounter = 0x00;
+ private static ThreadLocal LocalThreadId = new ThreadLocal(() => ++ThreadCounter);
+ private static int ThreadId => LocalThreadId.Value;
+#else
+ private static int ThreadId => Thread.CurrentThread.ManagedThreadId;
+#endif
+ public static bool IsWindows => RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
+ public static bool IsLinux => RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);
+ public static bool IsMac => RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX);
+
+ public AsyncMutexLock(string name, MutexScope scope = MutexScope.Machine)
+ {
+ this.FileName = LockFileName(name, scope);
+ }
+
+#if !DEBUG
+ readonly
+#endif
+ struct InnerLock : IDisposable
+ {
+ private readonly AsyncMutexLock _parent;
+ private readonly long _oldId;
+ private readonly int _oldThreadId;
+#if DEBUG
+ private bool _disposed;
+#endif
+
+ internal InnerLock(AsyncMutexLock parent, long oldId, int oldThreadId)
+ {
+ _parent = parent;
+ _oldId = oldId;
+ _oldThreadId = oldThreadId;
+#if DEBUG
+ _disposed = false;
+#endif
+ }
+
+ internal async Task ObtainLockAsync(CancellationToken cancellationToken = default)
+ {
+ while (true)
+ {
+ await _parent._reentrancy.WaitAsync(cancellationToken).ConfigureAwait(false);
+ if (InnerTryEnter(synchronous: false))
+ {
+ break;
+ }
+ // We need to wait for someone to leave the lock before trying again.
+ // We need to "atomically" obtain _retry and release _reentrancy, but there
+ // is no equivalent to a condition variable. Instead, we call *but don't await*
+ // _retry.WaitAsync(), then release the reentrancy lock, *then* await the saved task.
+ var waitTask = _parent._retry.WaitAsync(cancellationToken).ConfigureAwait(false);
+ _parent._reentrancy.Release();
+ await waitTask;
+ }
+
+ var oldThreadId = _parent._owningThreadId;
+ _parent._owningThreadId = ThreadId;
+
+ if (_parent._reentrances == 1) // Poll for mutex
+ {
+ _parent._reentrancy.Release();
+
+ while (true)
+ {
+ await _parent._reentrancy.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ if (TryMutexAcquireOnce())
+ {
+ break;
+ }
+ }
+ catch
+ {
+ _parent._owningThreadId = oldThreadId;
+ // we need to release retry here, since changing owningThreadId before we actually aquire the lock
+ // might cause other threads to wait on retry. It does not hurt if we release retry too much.
+ if (_parent._retry.CurrentCount == 0)
+ {
+ _parent._retry.Release();
+ }
+ _parent._reentrancy.Release();
+ throw;
+ }
+ _parent._reentrancy.Release();
+ await Task.Delay(pollMilliseconds, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ _parent._owningThreadId = ThreadId;
+ _parent._reentrancy.Release();
+ return this;
+ }
+
+ internal async Task TryObtainLockAsync(TimeSpan timeout)
+ {
+ // In case of zero-timeout, don't even wait for protective lock contention
+ if (timeout == TimeSpan.Zero)
+ {
+ if (!_parent._reentrancy.Wait(timeout)) return null;
+ if (InnerTryEnter(synchronous: false))
+ {
+ // Reset the owning thread id after all await calls have finished, otherwise we
+ // could be resumed on a different thread and set an incorrect value.
+ try
+ {
+ if (_parent._reentrances != 1 || TryMutexAcquireOnce())
+ {
+ _parent._owningThreadId = ThreadId;
+ _parent._reentrancy.Release();
+ return this;
+ }
+ }
+ catch (Exception)
+ {
+ _parent._reentrancy.Release();
+ throw;
+ }
+ }
+ _parent._reentrancy.Release();
+ return null;
+ }
+
+ var now = DateTimeOffset.UtcNow;
+ var last = now;
+ var remainder = timeout;
+
+ // We need to wait for someone to leave the lock before trying again.
+ while (remainder > TimeSpan.Zero)
+ {
+ if (!await _parent._reentrancy.WaitAsync(remainder).ConfigureAwait(false)) return null;
+ if (InnerTryEnter(synchronous: false))
+ {
+ var oldThreadId = _parent._owningThreadId;
+ _parent._owningThreadId = ThreadId;
+
+ if (_parent._reentrances == 1) // Poll for mutex
+ {
+ _parent._reentrancy.Release();
+
+ Task? reentrancyLock = null;
+ while (remainder > TimeSpan.Zero)
+ {
+ if (!await (reentrancyLock = _parent._reentrancy.WaitAsync(remainder)).ConfigureAwait(false))
+ {
+ await _parent._reentrancy.WaitAsync();
+ _parent._owningThreadId = oldThreadId;
+ // we need to release retry here, since changing owningThreadId before we actually aquire the lock
+ // might cause other threads to wait on retry. It does not hurt if we release retry too much.
+ if (_parent._retry.CurrentCount == 0)
+ {
+ _parent._retry.Release();
+ }
+ _parent._reentrancy.Release();
+ return null;
+ }
+ try
+ {
+ if (TryMutexAcquireOnce())
+ {
+ break;
+ }
+ }
+ catch
+ {
+ _parent._owningThreadId = oldThreadId;
+ // we need to release retry here, since changing owningThreadId before we actually aquire the lock
+ // might cause other threads to wait on retry. It does not hurt if we release retry too much.
+ if (_parent._retry.CurrentCount == 0)
+ {
+ _parent._retry.Release();
+ }
+ _parent._reentrancy.Release();
+ throw;
+ }
+
+ _parent._reentrancy.Release();
+
+ now = DateTimeOffset.UtcNow;
+ remainder -= now - last;
+ last = now;
+ var poll = TimeSpan.FromTicks(Math.Min(pollTimeSpan.Ticks, remainder.Ticks));
+ if (poll > TimeSpan.Zero) Thread.Sleep(poll);
+
+ now = DateTimeOffset.UtcNow;
+ remainder -= now - last;
+ last = now;
+ }
+ }
+
+ if (remainder > TimeSpan.Zero)
+ {
+ // Reset the owning thread id after all await calls have finished, otherwise we
+ // could be resumed on a different thread and set an incorrect value.
+ _parent._owningThreadId = ThreadId;
+ _parent._reentrancy.Release();
+ return this;
+ }
+ else
+ {
+ _parent._owningThreadId = oldThreadId;
+ // we need to release retry here, since changing owningThreadId before we actually aquire the lock
+ // might cause other threads to wait on retry. It does not hurt if we release retry too much.
+ if (_parent._retry.CurrentCount == 0)
+ {
+ _parent._retry.Release();
+ }
+ _parent._reentrancy.Release();
+ return null;
+ }
+ }
+
+ now = DateTimeOffset.UtcNow;
+ remainder -= now - last;
+ last = now;
+ if (remainder <= TimeSpan.Zero)
+ {
+ _parent._reentrancy.Release();
+ return null;
+ }
+
+ var waitTask = _parent._retry.WaitAsync(remainder).ConfigureAwait(false);
+ _parent._reentrancy.Release();
+ if (!await waitTask)
+ {
+ return null;
+ }
+
+ now = DateTimeOffset.UtcNow;
+ remainder -= now - last;
+ last = now;
+ }
+
+ return null;
+ }
+
+ internal async Task TryObtainLockAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ while (true)
+ {
+ await _parent._reentrancy.WaitAsync(cancellationToken).ConfigureAwait(false);
+ if (InnerTryEnter(synchronous: false))
+ {
+ break;
+ }
+ // We need to wait for someone to leave the lock before trying again.
+ var waitTask = _parent._retry.WaitAsync(cancellationToken).ConfigureAwait(false);
+ _parent._reentrancy.Release();
+ await waitTask;
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return null;
+ }
+
+ var oldThreadId = _parent._owningThreadId;
+ _parent._owningThreadId = ThreadId;
+
+ if (_parent._reentrances == 1) // Poll for mutex
+ {
+ _parent._reentrancy.Release();
+
+ try
+ {
+ while (true)
+ {
+ await _parent._reentrancy.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ if (TryMutexAcquireOnce()) break;
+ }
+ catch
+ {
+ _parent._owningThreadId = oldThreadId;
+ // we need to release retry here, since changing owningThreadId before we actually aquire the lock
+ // might cause other threads to wait on retry. It does not hurt if we release retry too much.
+ if (_parent._retry.CurrentCount == 0)
+ {
+ _parent._retry.Release();
+ }
+ _parent._reentrancy.Release();
+ throw;
+ }
+ _parent._reentrancy.Release();
+ await Task.Delay(pollMilliseconds, cancellationToken).ConfigureAwait(false); ;
+ }
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ _parent._owningThreadId = ThreadId;
+ _parent._reentrancy.Release();
+
+ return this;
+ }
+
+ internal IDisposable ObtainLock(CancellationToken cancellationToken = default)
+ {
+ while (true)
+ {
+ _parent._reentrancy.Wait(cancellationToken);
+ if (InnerTryEnter(synchronous: true))
+ {
+ break;
+ }
+ // We need to wait for someone to leave the lock before trying again.
+
+ _parent._reentrancy.Release();
+ _parent._retry.Wait(cancellationToken);
+ }
+
+ if (_parent._reentrances == 1) // Poll for mutex
+ {
+ _parent._reentrancy.Release();
+
+ while (true)
+ {
+ _parent._reentrancy.Wait(cancellationToken);
+ try
+ {
+ if (TryMutexAcquireOnce())
+ {
+ break;
+ }
+ }
+ catch
+ {
+ _parent._reentrancy.Release();
+ throw;
+ }
+ _parent._reentrancy.Release();
+
+ Thread.Sleep(pollMilliseconds);
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+ }
+
+ _parent._reentrancy.Release();
+ return this;
+ }
+
+ internal IDisposable? TryObtainLock(TimeSpan timeout)
+ {
+ // In case of zero-timeout, don't even wait for protective lock contention
+ if (timeout == TimeSpan.Zero)
+ {
+ if (!_parent._reentrancy.Wait(timeout)) return null;
+ if (InnerTryEnter(synchronous: true))
+ {
+ if (_parent._reentrances != 1 || TryMutexAcquireOnce())
+ {
+ _parent._reentrancy.Release();
+ return this;
+ }
+ }
+ _parent._reentrancy.Release();
+ return null;
+ }
+
+ var now = DateTimeOffset.UtcNow;
+ var last = now;
+ var remainder = timeout;
+
+ // We need to wait for someone to leave the lock before trying again.
+ while (remainder > TimeSpan.Zero)
+ {
+ if (!_parent._reentrancy.Wait(remainder)) return null;
+
+ now = DateTimeOffset.UtcNow;
+ remainder -= now - last;
+ last = now;
+
+ if (InnerTryEnter(synchronous: true))
+ {
+ if (_parent._reentrances == 1) // Poll for mutex
+ {
+ _parent._reentrancy.Release();
+
+ while (remainder > TimeSpan.Zero)
+ {
+ if (!_parent._reentrancy.Wait(remainder)) return null;
+ try
+ {
+ if (TryMutexAcquireOnce())
+ {
+ break;
+ }
+ }
+ catch
+ {
+ _parent._reentrancy.Release();
+ throw;
+ }
+
+ _parent._reentrancy.Release();
+
+ now = DateTimeOffset.UtcNow;
+ remainder -= now - last;
+ last = now;
+ var poll = TimeSpan.FromTicks(Math.Min(pollTimeSpan.Ticks, remainder.Ticks));
+ if (poll > TimeSpan.Zero) Thread.Sleep(poll);
+
+ now = DateTimeOffset.UtcNow;
+ remainder -= now - last;
+ last = now;
+ }
+
+ if (remainder <= TimeSpan.Zero) return null;
+ }
+
+ _parent._reentrancy.Release();
+ return this;
+ }
+
+ now = DateTimeOffset.UtcNow;
+ remainder -= now - last;
+ last = now;
+
+ _parent._reentrancy.Release();
+ if (!_parent._retry.Wait(remainder))
+ {
+ return null;
+ }
+
+ now = DateTimeOffset.UtcNow;
+ remainder -= now - last;
+ last = now;
+ }
+
+ return null;
+ }
+
+ private bool InnerTryEnter(bool synchronous)
+ {
+ if (synchronous)
+ {
+ if (_parent._owningThreadId == UnlockedId)
+ {
+ _parent._owningThreadId = ThreadId;
+ }
+ else if (_parent._owningThreadId != ThreadId)
+ {
+ // Another thread currently owns the lock
+ return false;
+ }
+ _parent._owningId = AsyncMutexLock.AsyncId;
+ }
+ else
+ {
+ if (_parent._owningId == UnlockedId)
+ {
+ _parent._owningId = AsyncMutexLock.AsyncId;
+ }
+ else if (_parent._owningId != _oldId)
+ {
+ // Another thread currently owns the lock
+ return false;
+ }
+ else
+ {
+ // Nested re-entrance
+ _parent._owningId = AsyncId;
+ }
+ }
+
+ // We can go in
+ _parent._reentrances += 1;
+ return true;
+ }
+
+ private bool TryMutexAcquireOnce(CancellationToken cancel = default) => _parent.TryMutexAcquireOnce(cancel);
+
+ public void Dispose()
+ {
+#if DEBUG
+ Debug.Assert(!_disposed);
+ _disposed = true;
+#endif
+ var @this = this;
+ var oldId = this._oldId;
+ var oldThreadId = this._oldThreadId;
+ @this._parent._reentrancy.Wait();
+ try
+ {
+ @this._parent._reentrances -= 1;
+ @this._parent._owningId = oldId;
+ @this._parent._owningThreadId = oldThreadId;
+ if (@this._parent._reentrances == 0)
+ {
+ // The owning thread is always the same so long as we
+ // are in a nested stack call. We reset the owning id
+ // only when the lock is fully unlocked.
+ @this._parent._owningId = UnlockedId;
+ @this._parent._owningThreadId = (int)UnlockedId;
+
+ _parent.MutexRelease();
+ }
+ // We can't place this within the _reentrances == 0 block above because we might
+ // still need to notify a parallel reentrant task to wake. I think.
+ // This should not be a race condition since we only wait on _retry with _reentrancy locked,
+ // then release _reentrancy so the Dispose() call can obtain it to signal _retry in a big hack.
+ if (@this._parent._retry.CurrentCount == 0)
+ {
+ @this._parent._retry.Release();
+ }
+ }
+ finally
+ {
+ @this._parent._reentrancy.Release();
+ }
+ }
+ }
+
+ // Mutex code
+ FileStream? LockFileStream;
+ int FlockFile = -1;
+
+ private const int LOCK_EX = 2;
+ private const int LOCK_NB = 4;
+ private const int LOCK_UN = 8;
+ private const int O_CREAT = 0x40;
+ private const int O_RDWR = 0x2;
+
+ const int pollMilliseconds = 100;
+ static readonly TimeSpan pollTimeSpan = TimeSpan.FromMilliseconds(pollMilliseconds);
+
+ [DllImport("libc", SetLastError = true)]
+ private static extern int flock(int fd, int operation);
+ [DllImport("libc", SetLastError = true)]
+ private static extern int open(string pathname, int flags, uint mode);
+
+ [DllImport("libc", SetLastError = true)]
+ private static extern int close(int fd);
+
+ const int MaxUnauthorizedAccessExceptionRetries = 1600;
+ private static bool CanRetryTransientFileSystemError(ref int retryCount)
+ {
+ if (retryCount >= MaxUnauthorizedAccessExceptionRetries) { return false; }
+
+ ++retryCount;
+
+ return true;
+ }
+ private void EnsureDirectoryExists()
+ {
+ var retryCount = 0;
+
+ var directory = Path.GetDirectoryName(FileName);
+ while (true)
+ {
+ try
+ {
+ Directory.CreateDirectory(directory);
+ return;
+ }
+ catch (Exception ex)
+ {
+ // This can indicate either a transient failure during concurrent creation/deletion or a permissions issue.
+ // If we encounter it, assume it is transient unless it persists.
+ // For a long time, I just checked for UnauthorizedAccessException here. However, recent tests on Linux have
+ // shown that in race conditions we can see IOException as well, presumably because there is some period during
+ // directory creation where it presents as a file.
+ if (ex is UnauthorizedAccessException or IOException
+ && CanRetryTransientFileSystemError(ref retryCount))
+ {
+ continue;
+ }
+
+ throw new InvalidOperationException($"Failed to ensure that lock file directory {directory} exists", ex);
+ }
+ }
+ }
+
+ internal bool TryMutexAcquireOnce(CancellationToken cancel = default)
+ {
+ if (IsWindows)
+ {
+ int retryCount = 0;
+
+ while (true)
+ {
+ cancel.ThrowIfCancellationRequested();
+
+ FileStream lockFileStream;
+ try
+ {
+ // key arguments:
+ // OpenOrCreate to be robust to the file existing or not
+ lockFileStream = new FileStream(FileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, bufferSize: 1);
+ try
+ {
+ lockFileStream.WriteByte(0);
+ lockFileStream.Flush();
+ lockFileStream.Lock(0, 1);
+ //AppDomain.CurrentDomain.ProcessExit += MutexRelease;
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ return false;
+ }
+ catch (IOException ex)
+ {
+ return false;
+ }
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // this should almost never happen because we just created the directory but in a race condition it could. Just retry
+ continue;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ // This can happen in few cases:
+
+ // The path is already directory, so we'll never be able to open a handle of it as a file
+ if (Directory.Exists(FileName))
+ {
+ throw new InvalidOperationException($"Failed to create lock file '{FileName}' because it is already the name of a directory");
+ }
+
+ // The file exists and is read-only
+ FileAttributes attributes;
+ try { attributes = File.GetAttributes(FileName); }
+ catch { attributes = FileAttributes.Normal; } // e. g. could fail with FileNotFoundException
+ if (attributes.HasFlag(FileAttributes.ReadOnly))
+ {
+ // We could support this by eschewing DeleteOnClose once we detect that a file is read-only,
+ // but absent interest or a use-case we'll just throw for now
+ throw new NotSupportedException($"Locking on read-only file '{FileName}' is not supported");
+ }
+
+ // Frustratingly, this error can be thrown transiently due to concurrent creation/deletion. Initially assume
+ // that it is transient and just retry
+ if (CanRetryTransientFileSystemError(ref retryCount))
+ {
+ continue;
+ }
+
+ // If we get here, we've exhausted our retries: assume that it is a legitimate permissions issue
+ throw;
+ }
+ // this should never happen because we validate. However if it does (e. g. due to some system configuration change?), throw so that
+ // this doesn't end up in the IOException block (PathTooLongException is IOException)
+ catch (PathTooLongException) { throw; }
+ catch (IOException)
+ {
+ // the hope is that if we get here the only failure reason would be that the file is locked
+ return false;
+ }
+
+ LockFileStream = lockFileStream;
+#if !NETSTANDARD1_3
+ AppDomain.CurrentDomain.ProcessExit += MutexRelease;
+#endif
+ return true;
+ }
+ }
+ else // Unix, use flock
+ {
+ int file = -1;
+ try
+ {
+ EnsureDirectoryExists();
+
+ file = open(FileName, O_CREAT | O_RDWR, 0x1A4); // 0644
+
+ if (file == -1) return false;
+
+ if (flock(file, LOCK_EX | LOCK_NB) == 0)
+ {
+ this.FlockFile = file;
+ return true;
+ }
+ else
+ {
+ var errno = Marshal.GetLastWin32Error();
+ }
+
+ return false;
+ }
+ finally
+ {
+ if (file != -1 && this.FlockFile != file) close(file);
+ }
+ }
+ }
+
+ internal void MutexRelease(object? sender = null, EventArgs? args = default)
+ {
+ if (IsWindows)
+ {
+ var file = Interlocked.Exchange(ref this.LockFileStream!, null);
+ if (file != null)
+ {
+ try
+ {
+ file.Unlock(0, 1);
+ file.Close();
+ file.Dispose();
+ AppDomain.CurrentDomain.ProcessExit -= MutexRelease;
+ }
+ catch (UnauthorizedAccessException) { }
+ catch (IOException) { }
+ }
+ }
+ else
+ {
+ if (FlockFile != -1)
+ {
+ flock(FlockFile, LOCK_UN);
+ close(FlockFile);
+
+ FlockFile = -1;
+ }
+ }
+ }
+
+ public void Dispose() => MutexRelease();
+ ~AsyncMutexLock() => Dispose();
+
+ // Make sure InnerLock.LockAsync() does not use await, because an async function triggers a snapshot of
+ // the AsyncLocal value.
+ public Task LockAsync(CancellationToken cancellationToken = default)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ _asyncId.Value = Interlocked.Increment(ref AsyncMutexLock.AsyncStackCounter);
+ return @lock.ObtainLockAsync(cancellationToken);
+ }
+
+ // Make sure InnerLock.LockAsync() does not use await, because an async function triggers a snapshot of
+ // the AsyncLocal value.
+ public Task TryLockAsync(Action callback, TimeSpan timeout)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ _asyncId.Value = Interlocked.Increment(ref AsyncMutexLock.AsyncStackCounter);
+
+ return @lock.TryObtainLockAsync(timeout)
+ .ContinueWith(state =>
+ {
+ if (state.Exception is AggregateException ex)
+ {
+ ExceptionDispatchInfo.Capture(ex.InnerException!).Throw();
+ }
+ var disposableLock = state.Result;
+ if (disposableLock is null)
+ {
+ return false;
+ }
+
+ try
+ {
+ callback();
+ }
+ finally
+ {
+ disposableLock.Dispose();
+ }
+ return true;
+ });
+ }
+
+ // Make sure InnerLock.LockAsync() does not use await, because an async function triggers a snapshot of
+ // the AsyncLocal value.
+ public Task TryLockAsync(Func callback, TimeSpan timeout)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ _asyncId.Value = Interlocked.Increment(ref AsyncMutexLock.AsyncStackCounter);
+
+ return @lock.TryObtainLockAsync(timeout)
+ .ContinueWith(state =>
+ {
+ if (state.Exception is AggregateException ex)
+ {
+ ExceptionDispatchInfo.Capture(ex.InnerException!).Throw();
+ }
+ var disposableLock = state.Result;
+ if (disposableLock is null)
+ {
+ return Task.FromResult(false);
+ }
+
+ return callback()
+ .ContinueWith(result =>
+ {
+ disposableLock.Dispose();
+
+ if (result.Exception is AggregateException ex)
+ {
+ ExceptionDispatchInfo.Capture(ex.InnerException!).Throw();
+ }
+
+ return true;
+ }, TaskScheduler.Default);
+ }, TaskScheduler.Default).Unwrap();
+ }
+
+ // Make sure InnerLock.TryLockAsync() does not use await, because an async function triggers a snapshot of
+ // the AsyncLocal value.
+ public Task TryLockAsync(Action callback, CancellationToken cancellationToken)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ _asyncId.Value = Interlocked.Increment(ref AsyncMutexLock.AsyncStackCounter);
+
+ return @lock.TryObtainLockAsync(cancellationToken)
+ .ContinueWith(state =>
+ {
+ if (state.Exception is AggregateException ex)
+ {
+ ExceptionDispatchInfo.Capture(ex.InnerException!).Throw();
+ }
+ var disposableLock = state.Result;
+ if (disposableLock is null)
+ {
+ return false;
+ }
+
+ try
+ {
+ callback();
+ }
+ finally
+ {
+ disposableLock.Dispose();
+ }
+ return true;
+ }, TaskScheduler.Default);
+ }
+
+ // Make sure InnerLock.LockAsync() does not use await, because an async function triggers a snapshot of
+ // the AsyncLocal value.
+ public Task TryLockAsync(Func callback, CancellationToken cancellationToken)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ _asyncId.Value = Interlocked.Increment(ref AsyncMutexLock.AsyncStackCounter);
+
+ return @lock.TryObtainLockAsync(cancellationToken)
+ .ContinueWith(state =>
+ {
+ if (state.Exception is AggregateException ex)
+ {
+ ExceptionDispatchInfo.Capture(ex.InnerException!).Throw();
+ }
+ var disposableLock = state.Result;
+ if (disposableLock is null)
+ {
+ return Task.FromResult(false);
+ }
+
+ return callback()
+ .ContinueWith(result =>
+ {
+ disposableLock.Dispose();
+
+ if (result.Exception is AggregateException ex)
+ {
+ ExceptionDispatchInfo.Capture(ex.InnerException!).Throw();
+ }
+
+ return true;
+ }, TaskScheduler.Default);
+ }, TaskScheduler.Default).Unwrap();
+ }
+
+ public IDisposable Lock(CancellationToken cancellationToken = default)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ // Increment the async stack counter to prevent a child task from getting
+ // the lock at the same time as a child thread.
+ _asyncId.Value = Interlocked.Increment(ref AsyncMutexLock.AsyncStackCounter);
+ return @lock.ObtainLock(cancellationToken);
+ }
+
+ public bool TryLock(Action callback, TimeSpan timeout)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ // Increment the async stack counter to prevent a child task from getting
+ // the lock at the same time as a child thread.
+ _asyncId.Value = Interlocked.Increment(ref AsyncMutexLock.AsyncStackCounter);
+ var lockDisposable = @lock.TryObtainLock(timeout);
+ if (lockDisposable is null)
+ {
+ return false;
+ }
+
+ // Execute the callback then release the lock
+ try
+ {
+ callback();
+ }
+ finally
+ {
+ lockDisposable.Dispose();
+ }
+ return true;
+ }
+
+ public Task LockAsync(TimeSpan timeout)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ _asyncId.Value = Interlocked.Increment(ref AsyncMutexLock.AsyncStackCounter);
+
+ return @lock.TryObtainLockAsync(timeout)
+ .ContinueWith(state =>
+ {
+ if (state.Exception is AggregateException ex)
+ {
+ ExceptionDispatchInfo.Capture(ex.InnerException!).Throw();
+ }
+ var disposableLock = state.Result;
+ if (disposableLock is null) throw new TimeoutException("LockAsync timed out.");
+ return disposableLock;
+ });
+ }
+
+ public IDisposable Lock(TimeSpan timeout)
+ {
+ var @lock = new InnerLock(this, _asyncId.Value, ThreadId);
+ // Increment the async stack counter to prevent a child task from getting
+ // the lock at the same time as a child thread.
+ _asyncId.Value = Interlocked.Increment(ref AsyncMutexLock.AsyncStackCounter);
+ var lockDisposable = @lock.TryObtainLock(timeout);
+ if (lockDisposable is null) throw new TimeoutException("TryLock timed out.");
+ return lockDisposable;
+ }
+
+ [DllImport("libc")]
+ public static extern uint getuid();
+
+ public static bool UnixIsRoot => getuid() == 0;
+
+ public static string LockFileName(string name, MutexScope scope = MutexScope.Machine)
+ {
+ if (Path.IsPathRooted(name)) return name;
+#if NETSTANDARD1_3
+ throw new NotSupportedException("Only full filenames are supported as name on netstandard1.3");
+#else
+ if (name.StartsWith("Local\\", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("AsyncMutexLock does not support local mutexes");
+ //if (IsWindows) return $"Global\\{name.Replace('/', '_')}";
+
+ var pattern = @"[ $%&""'=?!^_:\t\r\n\\/]";
+ pattern = Path.DirectorySeparatorChar == '\\' ?
+ @"[ $%&""'=?!^_:\t\r\n/]" :
+ pattern.Replace(Path.DirectorySeparatorChar.ToString(), "");
+
+ name = Regex.Replace(name, pattern, "-");
+
+ if (scope == MutexScope.User)
+ {
+ var root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ var lockpath = Path.Combine(root, "asyncmutexlock");
+ var lockfile = Path.Combine(lockpath, $"{name}.lock");
+ Directory.CreateDirectory(lockpath);
+ return lockfile;
+ }
+ else
+ {
+ var root = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
+ var lockpath = Path.Combine(root, "asyncmutexlock");
+ var lockfile = Path.Combine(lockpath, $"{name}.lock");
+ Directory.CreateDirectory(lockpath);
+ return lockfile;
+ }
+#endif
+ }
+}
+#endif
\ No newline at end of file
diff --git a/README.md b/README.md
index 2c773bc..5b1c3cb 100644
--- a/README.md
+++ b/README.md
@@ -69,3 +69,8 @@ private class AsyncLockTest
}
}
```
+
+## Async Global Mutex
+The class `AsyncMutexLock` works exactly like `AsyncLock`, except that it uses a cross process machine wide file lock.
+It can be used as an async friendly version of a global Mutex. For this, you create a named lock by calling the constructor
+`AsyncMutexLock("MyMutexName")`. You can then synchronize proecesses using this global lock.
\ No newline at end of file
diff --git a/UnitTests/AsyncMutexLock/AsyncIdTests.cs b/UnitTests/AsyncMutexLock/AsyncIdTests.cs
new file mode 100644
index 0000000..9cdd699
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/AsyncIdTests.cs
@@ -0,0 +1,82 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using AsyncLockTests;
+
+#if false
+namespace AsyncLockTests.Mutex;
+
+[TestClass]
+public class AsyncIdTests
+{
+ [TestMethod]
+ public void TaskIdUniqueness()
+ {
+ var testCount = 100;
+ var countdown = new CountdownEvent(testCount);
+ var failure = new ManualResetEventSlim(false);
+ var threadIds = new SortedSet();
+ var abort = new SemaphoreSlim(0, 1);
+
+ for (int i = 0; i < testCount; ++i)
+ {
+ Task.Run(async () =>
+ {
+ lock (threadIds)
+ {
+ if (!threadIds.Add(AsyncMutexLock.ThreadId))
+ {
+ failure.Set();
+ }
+ }
+ countdown.Signal();
+ await abort.WaitAsync();
+ });
+ }
+
+ if (WaitHandle.WaitAny(new[] { countdown.WaitHandle, failure.WaitHandle }) == 1)
+ {
+ Assert.Fail("A duplicate thread id was found!");
+ }
+
+ abort.Release();
+ }
+
+ public void ThreadIdUniqueness()
+ {
+ var testCount = 100;
+ var countdown = new CountdownEvent(testCount);
+ var failure = new ManualResetEventSlim(false);
+ var threadIds = new SortedSet();
+ var abort = new SemaphoreSlim(0, 1);
+
+ for (int i = 0; i < testCount; ++i)
+ {
+ Task.Run(async () =>
+ {
+ lock (threadIds)
+ {
+ if (!threadIds.Add(AsyncMutexLock.ThreadId))
+ {
+ failure.Set();
+ }
+ }
+ countdown.Signal();
+ await abort.WaitAsync();
+ });
+ }
+
+ if (WaitHandle.WaitAny(new[] { countdown.WaitHandle, failure.WaitHandle }) == 1)
+ {
+ Assert.Fail("A duplicate thread id was found!");
+ }
+
+ abort.Release();
+ }
+}
+#endif
diff --git a/UnitTests/AsyncMutexLock/AsyncSpawn.cs b/UnitTests/AsyncMutexLock/AsyncSpawn.cs
new file mode 100644
index 0000000..05dd831
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/AsyncSpawn.cs
@@ -0,0 +1,78 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsyncLockTests;
+
+namespace AsyncLockTests.Mutex;
+
+///
+/// Creates multiple independent tasks, each with its own lock, and runs them
+/// all in parallel. There should be no contention for the lock between the
+/// parallelly executed tasks, but each task then recursively obtains what
+/// should be the same lock - which should again be contention-free - after
+/// an await point that may or may not resume on the same actual thread the
+/// previous lock was obtained with.
+///
+[TestClass]
+public class AsyncSpawn
+{
+ public readonly struct NullDisposable : IDisposable
+ {
+ public void Dispose() { }
+ }
+
+ public async Task AsyncExecution(bool locked)
+ {
+ var count = 0;
+ var tasks = new List(70);
+ var asyncLock = new AsyncMutexLock(nameof(AsyncSpawn));
+ var rng = new Random();
+
+ {
+ using var l = locked ? await asyncLock.LockAsync() : new NullDisposable();
+
+ for (int i = 0; i < 10; ++i)
+ {
+ var task = Task.Run(async () =>
+ {
+ using (await asyncLock.LockAsync())
+ {
+ Assert.AreEqual(1, Interlocked.Increment(ref count));
+ await Task.Yield();
+ Assert.AreEqual(1, count);
+ await Task.Delay(rng.Next(1, 10) * 10);
+ using (await asyncLock.LockAsync())
+ {
+ await Task.Delay(rng.Next(1, 10) * 10);
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
+ }
+
+ Assert.AreEqual(0, count);
+ }
+
+ });
+ tasks.Add(task);
+ }
+ }
+
+ await Task.WhenAll(tasks);
+
+ Assert.AreEqual(0, count);
+ }
+
+ [TestMethod]
+ public async Task AsyncExecutionLocked()
+ {
+ await AsyncExecution(true);
+ }
+
+ [TestMethod]
+ public async Task AsyncExecutionUnlocked()
+ {
+ await AsyncExecution(false);
+ }
+}
diff --git a/UnitTests/AsyncMutexLock/CancellationTests.cs b/UnitTests/AsyncMutexLock/CancellationTests.cs
new file mode 100644
index 0000000..493e0e0
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/CancellationTests.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using AsyncLockTests;
+
+namespace AsyncLockTests.Mutex;
+
+[TestClass]
+public class CancellationTests
+{
+ [TestMethod]
+ public void CancellingWait()
+ {
+ var @lock = new AsyncMutexLock(nameof(CancellationTests));
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
+ Task.Run(async () =>
+ {
+ await @lock.LockAsync(cts.Token);
+ }).Wait();
+ Assert.ThrowsAsync(async () =>
+ {
+ using (await @lock.LockAsync(cts.Token))
+ Assert.Fail("should never reach here if cancellation works properly");
+ }).Wait();
+
+ }
+
+ [TestMethod]
+ public void CancellingWaitSync()
+ {
+ var asyncLock = new AsyncLock();
+ var cts = new CancellationTokenSource(250);
+ var delayStarted = new ManualResetEventSlim(false);
+ var waiter1Finished = new SemaphoreSlim(0, 1);
+
+ new Thread(() =>
+ {
+ using (asyncLock.Lock(cts.Token))
+ {
+ // hold the lock until our later attempt is called
+ delayStarted.Set();
+ waiter1Finished.Wait();
+ }
+ }).Start();
+
+ Assert.Throws(() =>
+ {
+ delayStarted.Wait();
+ using (asyncLock.Lock(cts.Token))
+ {
+ Assert.Fail("should never reach here if cancellation works properly.");
+ }
+ });
+ waiter1Finished.Release(1);
+
+ // We should still be able to obtain a lock afterward to make sure resources were reobtained
+ var newCts = new CancellationTokenSource(2000);
+ using (asyncLock.Lock(newCts.Token))
+ {
+ // reaching this line means the test passed
+ // a OperationCanceledException will indicate test failure
+ }
+ }
+}
diff --git a/UnitTests/AsyncMutexLock/MixedSyncAsync.cs b/UnitTests/AsyncMutexLock/MixedSyncAsync.cs
new file mode 100644
index 0000000..7369f6c
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/MixedSyncAsync.cs
@@ -0,0 +1,109 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsyncLockTests;
+using System.Diagnostics;
+
+namespace AsyncLockTests.Mutex;
+
+///
+/// Creates multiple indepndent tasks, each with its own lock, and runs them
+/// all in parallel. There should be no contention for the lock between the
+/// parallelly executed tasks, but each task then recursively obtains what
+/// should be the same lock - which should again be contention-free - after
+/// an await point that may or may not resume on the same actual thread the
+/// previous lock was obtained with.
+///
+[TestClass]
+public class MixedSyncAsync
+{
+ [TestMethod]
+ public async Task MixedSyncAsyncExecution()
+ {
+ int count = 0, nsync = 0, nasync = 0;
+ var threads = new List(10);
+ var tasks = new List(10);
+ var asyncLock = new AsyncMutexLock(nameof(MixedSyncAsync));
+ var rng = new Random();
+ var start = DateTime.UtcNow;
+
+ {
+ //using var l = asyncLock.Lock();
+ for (int i = 0; i < 10; ++i)
+ {
+ var thread = new Thread(() =>
+ {
+ var id = Interlocked.Increment(ref nsync);
+ Debug.WriteLine($"Sync Thread: {id}; Sync Thread ID: {Thread.CurrentThread.ManagedThreadId}");
+ var t0 = DateTime.UtcNow;
+ using (asyncLock.Lock())
+ {
+ var time = DateTime.UtcNow - t0;
+ Debug.WriteLine($"SyncLock{id}.1: {time}");
+
+ Assert.AreEqual(1, Interlocked.Increment(ref count));
+ Thread.Sleep(rng.Next(1, 10) * 10);
+ t0 = DateTime.UtcNow;
+ using (asyncLock.Lock())
+ {
+ time = DateTime.UtcNow - t0;
+ Debug.WriteLine($"SyncLock{id}.2: {time}");
+
+ Thread.Sleep(10);
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
+ }
+
+ Assert.AreEqual(0, count);
+ }
+
+ });
+ thread.Start();
+ threads.Add(thread);
+ }
+
+ for (int i = 0; i < 10; ++i)
+ {
+ var task = Task.Run(async () =>
+ {
+ var id = Interlocked.Increment(ref nasync);
+ Debug.WriteLine($"Async Thread: {id}; Async Thread ID: {Thread.CurrentThread.ManagedThreadId}");
+
+ var t0 = DateTime.UtcNow;
+ using (await asyncLock.LockAsync())
+ {
+ var time = DateTime.UtcNow - t0;
+ Debug.WriteLine($"AsyncLock{id}.1: {time}");
+
+ Assert.AreEqual(1, Interlocked.Increment(ref count));
+ Assert.AreEqual(1, count);
+ await Task.Delay(rng.Next(1, 10) * 10);
+ t0 = DateTime.UtcNow;
+ using (await asyncLock.LockAsync())
+ {
+ time = DateTime.UtcNow - t0;
+ Debug.WriteLine($"AsyncLock{id}.2: {time}");
+ await Task.Delay(10);
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
+ }
+
+ Assert.AreEqual(0, count);
+ }
+
+ });
+ tasks.Add(task);
+ }
+ }
+
+ await Task.WhenAll(tasks);
+ foreach (var thread in threads)
+ {
+ thread.Join();
+ }
+
+ Assert.AreEqual(0, count);
+ }
+}
diff --git a/UnitTests/AsyncMutexLock/MixedSyncAsyncTimed.cs b/UnitTests/AsyncMutexLock/MixedSyncAsyncTimed.cs
new file mode 100644
index 0000000..abb7c93
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/MixedSyncAsyncTimed.cs
@@ -0,0 +1,108 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AsyncLockTests;
+
+namespace AsyncLockTests.Mutex;
+
+///
+/// Creates multiple indepndent tasks, each with its own lock, and runs them
+/// all in parallel. There should be no contention for the lock between the
+/// parallelly executed tasks, but each task then recursively obtains what
+/// should be the same lock - which should again be contention-free - after
+/// an await point that may or may not resume on the same actual thread the
+/// previous lock was obtained with.
+///
+[TestClass]
+public class MixedSyncAsyncTimed
+{
+ [TestMethod]
+ public async Task MixedSyncAsyncExecution()
+ {
+ var count = 0;
+ var threads = new List(10);
+ var tasks = new List(10);
+ var asyncLock = new AsyncMutexLock(nameof(MixedSyncAsyncTimed));
+ var rng = new Random();
+
+ {
+ using var l = asyncLock.Lock();
+ for (int i = 0; i < 10; ++i)
+ {
+ var thread = new Thread(() =>
+ {
+ using (asyncLock.Lock())
+ {
+ Assert.AreEqual(1, Interlocked.Increment(ref count));
+ Thread.Sleep(rng.Next(1, 10) * 10);
+ using (asyncLock.Lock())
+ {
+ Thread.Sleep(10);
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
+ }
+
+ Assert.AreEqual(0, count);
+ }
+
+ });
+ thread.Start();
+ threads.Add(thread);
+ }
+
+ for (int i = 0; i < 10; ++i)
+ {
+ var captured = i;
+ var task = Task.Run(async () =>
+ {
+ using (await asyncLock.LockAsync())
+ {
+ Assert.AreEqual(1, Interlocked.Increment(ref count));
+ Assert.AreEqual(1, count);
+ await Task.Delay(rng.Next(1, 10) * 10);
+ if (captured % 2 == 0)
+ {
+ using (await asyncLock.LockAsync())
+ {
+ await Task.Yield();
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
+ }
+ }
+ else
+ {
+ var executed = await asyncLock.TryLockAsync(async () =>
+ {
+ // Throw in a recursive async lock invocation
+ bool nestedExecuted = await asyncLock.TryLockAsync(async () =>
+ {
+ await Task.Yield();
+ Interlocked.Increment(ref count);
+ }, TimeSpan.FromMilliseconds(1 /* guarantees no zero-ms optimizations */));
+ Assert.IsTrue(nestedExecuted);
+ Interlocked.Decrement(ref count);
+ await Task.Yield();
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
+ }, TimeSpan.FromMilliseconds(rng.Next(1, 10) * 10));
+ Assert.IsTrue(executed, "TryLockAsync() did not end up executing!");
+ }
+
+ Assert.AreEqual(0, count);
+ }
+
+ });
+ tasks.Add(task);
+ }
+ }
+
+ await Task.WhenAll(tasks);
+ foreach (var thread in threads)
+ {
+ thread.Join();
+ }
+
+ Assert.AreEqual(0, count);
+ }
+}
diff --git a/UnitTests/AsyncMutexLock/ParallelExecutionTests.cs b/UnitTests/AsyncMutexLock/ParallelExecutionTests.cs
new file mode 100644
index 0000000..9abda7b
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/ParallelExecutionTests.cs
@@ -0,0 +1,43 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using AsyncLockTests;
+
+namespace AsyncLockTests.Mutex;
+
+///
+/// Creates multiple indepndent tasks, each with its own lock, and runs them
+/// all in parallel. There should be no contention for the lock between the
+/// parallelly executed tasks, but each task then recursively obtains what
+/// should be the same lock - which should again be contention-free - after
+/// an await point that may or may not resume on the same actual thread the
+/// previous lock was obtained with.
+///
+[TestClass]
+public class ParallelExecutionTests
+{
+ [TestMethod]
+ public async Task ParallelExecution()
+ {
+ await Task.WhenAll(Enumerable.Range(0, 3).Select(SomeMethod));
+ }
+
+ private static async Task SomeMethod(int i)
+ {
+ var asyncLock = new AsyncMutexLock(nameof(ParallelExecution));
+ System.Diagnostics.Debug.WriteLine($"Outside {i}");
+ await Task.Delay(100);
+ using (await asyncLock.LockAsync())
+ {
+ System.Diagnostics.Debug.WriteLine($"Lock1 {i}");
+ await Task.Delay(100);
+ using (await asyncLock.LockAsync())
+ {
+ System.Diagnostics.Debug.WriteLine($"Lock2 {i}");
+ await Task.Delay(100);
+ }
+ }
+ }
+}
diff --git a/UnitTests/AsyncMutexLock/ReentracePermittedTests.cs b/UnitTests/AsyncMutexLock/ReentracePermittedTests.cs
new file mode 100644
index 0000000..b17b860
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/ReentracePermittedTests.cs
@@ -0,0 +1,75 @@
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using AsyncLockTests;
+
+namespace AsyncLockTests.Mutex;
+
+[TestClass]
+public class ReentracePermittedTests
+{
+ readonly AsyncMutexLock _lock = new AsyncMutexLock(nameof(ReentracePermittedTests));
+
+ [TestMethod]
+ public async Task NestedCallReentrance()
+ {
+ using (await _lock.LockAsync())
+ using (await _lock.LockAsync())
+ {
+ Debug.WriteLine("Hello from NestedCallReentrance!");
+ }
+ }
+
+ [TestMethod]
+ public void NestedAsyncCallReentrance()
+ {
+ var task = Task.Run(async () =>
+ {
+ using (await _lock.LockAsync())
+ using (await _lock.LockAsync())
+ {
+ Debug.WriteLine("Hello from NestedCallReentrance!");
+ }
+ });
+
+ new TaskWaiter(task).WaitOne();
+ }
+
+ private async Task NestedFunctionAsync()
+ {
+ using (await _lock.LockAsync())
+ {
+ Debug.WriteLine("Hello from another (nested) function!");
+ }
+ }
+
+ [TestMethod]
+ public async Task NestedFunctionCallReentrance()
+ {
+ using (await _lock.LockAsync())
+ {
+ await NestedFunctionAsync();
+ }
+ }
+
+ // Issue #18
+ [TestMethod]
+ //[Timeout(5)]
+ public async Task BackToBackReentrance()
+ {
+ var asyncLock = new AsyncLock();
+ async Task InnerFunctionAsync()
+ {
+ using (await asyncLock.LockAsync())
+ {
+ //
+ }
+ }
+ using (await asyncLock.LockAsync())
+ {
+ await InnerFunctionAsync();
+ await InnerFunctionAsync();
+ }
+ }
+}
diff --git a/UnitTests/AsyncMutexLock/ReentranceLockoutTests.cs b/UnitTests/AsyncMutexLock/ReentranceLockoutTests.cs
new file mode 100644
index 0000000..3c7c9b3
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/ReentranceLockoutTests.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using AsyncLockTests;
+
+namespace AsyncLockTests.Mutex;
+
+[TestClass]
+public class ReentranceLockoutTests
+{
+ private AsyncMutexLock _lock;
+ private LimitedResource _resource;
+ private CountdownEvent _countdown;
+ private Random _random = new Random((int)DateTime.UtcNow.Ticks);
+ private int DelayInterval => _random.Next(1, 5) * 10;
+
+ private void ResourceSimulation(Action action)
+ {
+ _lock = new AsyncMutexLock(nameof(ReentracePermittedTests));
+ // Start n threads and have them obtain the lock and randomly wait, then verify
+ var failure = new ManualResetEventSlim(false);
+ _resource = new LimitedResource(() =>
+ {
+ failure.Set();
+ });
+
+ var testCount = 20;
+ _countdown = new CountdownEvent(testCount);
+
+ for (int i = 0; i < testCount; ++i)
+ {
+ action();
+ }
+
+ if (WaitHandle.WaitAny(new[] { _countdown.WaitHandle, failure.WaitHandle }) == 1)
+ {
+ Assert.Fail("More than one thread simultaneously accessed the underlying resource!");
+ }
+ }
+
+ private async void ThreadEntryPoint()
+ {
+ using (await _lock.LockAsync())
+ {
+ _resource.BeginSomethingDangerous();
+ Thread.Sleep(DelayInterval);
+ _resource.EndSomethingDangerous();
+ }
+ _countdown.Signal();
+ }
+
+ ///
+ /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing a function entrypoint.
+ ///
+ [TestMethod]
+ public void MultipleThreadsMethodLockout()
+ {
+ ResourceSimulation(() =>
+ {
+ var t = new Thread(ThreadEntryPoint);
+ t.Start();
+ });
+ }
+
+ ///
+ /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing nothing.
+ ///
+ [TestMethod]
+ public void MultipleThreadsLockout()
+ {
+ ResourceSimulation(() =>
+ {
+ var t = Task.Run(async () =>
+ {
+ using (await _lock.LockAsync())
+ {
+ _resource.BeginSomethingDangerous();
+ Thread.Sleep(DelayInterval);
+ _resource.EndSomethingDangerous();
+ }
+ _countdown.Signal();
+ });
+ });
+ }
+
+ ///
+ /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing a local ThreadStart
+ ///
+ [TestMethod]
+ public void MultipleThreadsThreadStartLockout()
+ {
+ ThreadStart work = async () =>
+ {
+ using (await _lock.LockAsync())
+ {
+ _resource.BeginSomethingDangerous();
+ Thread.Sleep(DelayInterval);
+ _resource.EndSomethingDangerous();
+ }
+ _countdown.Signal();
+ };
+
+ ResourceSimulation(() =>
+ {
+ var t = new Thread(work);
+ t.Start();
+ });
+ }
+
+ [TestMethod]
+ public void AsyncLockout()
+ {
+ ResourceSimulation(() =>
+ {
+ Task.Run(async () =>
+ {
+ using (await _lock.LockAsync())
+ {
+ _resource.BeginSomethingDangerous();
+ Thread.Sleep(DelayInterval);
+ _resource.EndSomethingDangerous();
+ }
+ _countdown.Signal();
+ });
+ });
+ }
+
+ [TestMethod]
+ public void AsyncDelayLockout()
+ {
+ ResourceSimulation(() =>
+ {
+ Task.Run(async () =>
+ {
+ using (await _lock.LockAsync())
+ {
+ _resource.BeginSomethingDangerous();
+ await Task.Delay(DelayInterval);
+ _resource.EndSomethingDangerous();
+ }
+ _countdown.Signal();
+ });
+ });
+ }
+
+ [TestMethod]
+ public async Task NestedAsyncLockout()
+ {
+ var taskStarted = new SemaphoreSlim(0, 1);
+ var taskEnded = new SemaphoreSlim(0, 1);
+ var @lock = new AsyncMutexLock(nameof(ReentracePermittedTests));
+ using (await @lock.LockAsync())
+ {
+ var task = Task.Run(async () =>
+ {
+ taskStarted.Release();
+ using (await @lock.LockAsync())
+ {
+ Debug.WriteLine("Hello from within an async task!");
+ }
+ await taskEnded.WaitAsync();
+ });
+
+ taskStarted.Wait();
+ Assert.IsFalse(new TaskWaiter(task).WaitOne(100));
+ taskEnded.Release();
+ }
+ }
+}
diff --git a/UnitTests/AsyncMutexLock/TryLockTests.cs b/UnitTests/AsyncMutexLock/TryLockTests.cs
new file mode 100644
index 0000000..c06a489
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/TryLockTests.cs
@@ -0,0 +1,83 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using System;
+using System.Threading;
+using AsyncLockTests;
+
+namespace AsyncLockTests.Mutex;
+
+[TestClass]
+public class TryLockTests
+{
+ [TestMethod]
+ public void NoContention()
+ {
+ using var @lock = new AsyncMutexLock(nameof(TryLockTests));
+
+ Assert.IsTrue(@lock.TryLock(() => { }, default));
+ }
+
+ [TestMethod]
+ public void ContentionEarlyReturn()
+ {
+ using var @lock = new AsyncMutexLock(nameof(TryLockTests));
+
+ using (@lock.Lock())
+ {
+ var thread = new Thread(() =>
+ {
+ Assert.IsFalse(@lock.TryLock(() => throw new Exception("This should never be executed"), default));
+ });
+ thread.Start();
+ thread.Join();
+ }
+ }
+
+ [TestMethod]
+ public void ContentionDelayedExecution() => ContentionalExecution(50, 250, true);
+
+ [TestMethod]
+ public void ContentionNoExecution() => ContentionalExecution(250, 50, false);
+
+ [TestMethod]
+ public void ContentionNoExecutionZeroTimeout() => ContentionalExecution(250, 0, false);
+
+ private void ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult)
+ {
+ int step = 0;
+ using var @lock = new AsyncMutexLock(nameof(TryLockTests));
+
+ var locked = @lock.Lock();
+ Interlocked.Increment(ref step);
+
+ using var eventTestThreadStarted = new AutoResetEvent(false);
+ using var eventSleepNotStarted = new AutoResetEvent(false);
+ using var eventAboutToWait = new AutoResetEvent(false);
+
+ var unlockThread = new Thread(() =>
+ {
+ eventTestThreadStarted.WaitOne();
+ eventSleepNotStarted.Set();
+ Thread.Sleep(unlockDelayMs);
+ eventAboutToWait.WaitOne();
+ Interlocked.Increment(ref step);
+ locked.Dispose();
+ });
+ unlockThread.Start();
+
+ var testThread = new Thread(() =>
+ {
+ eventTestThreadStarted.Set();
+ eventSleepNotStarted.WaitOne();
+ eventAboutToWait.Set();
+ Assert.IsTrue((!expectedResult) ^ @lock.TryLock(() =>
+ {
+ Assert.AreEqual(2, step);
+ }, TimeSpan.FromMilliseconds(lockTimeoutMs)));
+ });
+ testThread.Start();
+
+ unlockThread.Join();
+ testThread.Join();
+ }
+}
diff --git a/UnitTests/AsyncMutexLock/TryLockTestsAsync.cs b/UnitTests/AsyncMutexLock/TryLockTestsAsync.cs
new file mode 100644
index 0000000..2851780
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/TryLockTestsAsync.cs
@@ -0,0 +1,123 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AsyncLockTests.Mutex;
+
+class LocalException : Exception {
+ public LocalException(string message) : base(message) { }
+}
+
+[TestClass]
+public class TryLockTestsAsync
+{
+ [TestMethod]
+ public async Task NoContention()
+ {
+ var @lock = new AsyncMutexLock("test");
+
+ Assert.IsTrue(await @lock.TryLockAsync(() => { }, TimeSpan.Zero));
+ }
+
+ ///
+ /// Assert that exceptions are bubbled up after the lock is disposed
+ ///
+ ///
+ [TestMethod]
+ public async Task NoContentionThrows()
+ {
+ var @lock = new AsyncMutexLock("test");
+
+ await Assert.ThrowsAsync(async () =>
+ {
+ await @lock.TryLockAsync(async () => {
+ await Task.Yield();
+ throw new LocalException("This exception needs to be bubbled up");
+ }, TimeSpan.Zero);
+ });
+ }
+
+ [TestMethod]
+ public async Task ContentionEarlyReturn()
+ {
+ var @lock = new AsyncMutexLock(nameof(ContentionEarlyReturn));
+ var finished = new TaskCompletionSource();
+
+ using (await @lock.LockAsync())
+ {
+ var task = new Thread(async () =>
+ {
+ try
+ {
+ Assert.IsFalse(await @lock.TryLockAsync(() => throw new Exception("This should be executed"), TimeSpan.Zero));
+ }
+ catch (Exception ex)
+ {
+ finished.SetException(ex);
+ return;
+ }
+ finished.SetResult();
+ });
+ task.Start();
+ task.Join();
+ try
+ {
+ await finished.Task;
+ Assert.Fail("Exception should throw.");
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ //[TestMethod] broken. Did seem to work before because exception was swallowed inside Thread
+ public async Task ContentionDelayedExecution() => await ContentionalExecution(50, 250, true);
+
+ //[TestMethod] broken. Did seem to work before because exception was swallowed inside Thread
+ public async Task ContentionNoExecution() => await ContentionalExecution(250, 50, false);
+
+ //[TestMethod] broken. Did seem to work before because exception was swallowed inside Thread
+ public async Task ContentionNoExecutionZeroTimeout() => await ContentionalExecution(250, 0, false);
+
+ private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult)
+ {
+ int step = 0;
+ var @lock = new AsyncMutexLock("test");
+
+ var locked = await @lock.LockAsync();
+ Interlocked.Increment(ref step);
+
+ using var eventTestThreadStarted = new SemaphoreSlim(0, 1);
+ using var eventSleepNotStarted = new SemaphoreSlim(0, 1);
+ using var eventAboutToWait = new SemaphoreSlim(0, 1);
+
+ var unlockThread = new Thread(async () =>
+ {
+ await eventTestThreadStarted.WaitAsync();
+ eventSleepNotStarted.Release();
+ Thread.Sleep(unlockDelayMs);
+ await eventAboutToWait.WaitAsync();
+ Interlocked.Increment(ref step);
+ locked.Dispose();
+ });
+ unlockThread.Start();
+
+ var testThread = new Thread(async () =>
+ {
+ eventTestThreadStarted.Release();
+ await eventSleepNotStarted.WaitAsync();
+ eventAboutToWait.Release();
+ Assert.IsTrue((!expectedResult) ^ await @lock.TryLockAsync(() =>
+ {
+ Assert.AreEqual(2, step);
+ }, TimeSpan.FromMilliseconds(lockTimeoutMs)));
+ });
+ testThread.Start();
+
+ unlockThread.Join();
+ testThread.Join();
+ }
+}
diff --git a/UnitTests/AsyncMutexLock/TryLockTestsAsyncOut.cs b/UnitTests/AsyncMutexLock/TryLockTestsAsyncOut.cs
new file mode 100644
index 0000000..4c129e8
--- /dev/null
+++ b/UnitTests/AsyncMutexLock/TryLockTestsAsyncOut.cs
@@ -0,0 +1,113 @@
+#if TRY_LOCK_OUT_BOOL
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NeoSmart.AsyncLock;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using AsyncLockTests;
+
+namespace AsyncLockTests.Mutex;
+
+[TestClass]
+public class TryLockTestsAsyncOut
+{
+ [TestMethod]
+ public async Task NoContention()
+ {
+ using var @lock = new AsyncMutexLock(nameof(TryLockTestsAsyncOut));
+
+ Assert.IsTrue(await @lock.TryLockAsync(() => { }, TimeSpan.Zero));
+ }
+
+ ///
+ /// Assert that exceptions are bubbled up after the lock is disposed
+ ///
+ ///
+ [TestMethod]
+ public async Task NoContentionThrows()
+ {
+ using var @lock = new AsyncMutexLock(nameof(TryLockTestsAsyncOut));
+
+ await Assert.ThrowsExceptionAsync(async () =>
+ {
+ using (await @lock.TryLockAsync(TimeSpan.Zero, out var locked))
+ {
+ if (locked)
+ {
+ await Task.Yield();
+ throw new LocalException("This exception needs to be bubbled up");
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public async Task ContentionEarlyReturn()
+ {
+ using var @lock = new AsyncMutexLock(nameof(TryLockTestsAsyncOut));
+
+ using (await @lock.LockAsync())
+ {
+ var thread = Task.Run(async () =>
+ {
+ await Task.Yield();
+ var disposable = @lock.TryLockAsync(TimeSpan.Zero, out var locked);
+ Assert.IsFalse(locked);
+ });
+ await thread;
+ }
+ }
+
+ [TestMethod]
+ public async Task ContentionDelayedExecution() => await ContentionalExecution(50, 250, true);
+
+ [TestMethod]
+ public async Task ContentionNoExecution() => await ContentionalExecution(250, 50, false);
+
+ [TestMethod]
+ public async Task ContentionNoExecutionZeroTimeout() => await ContentionalExecution(250, 0, false);
+
+ private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult)
+ {
+ int step = 0;
+ using var @lock = new AsyncMutexLock(nameof(TryLockTestsAsyncOut));
+
+ var locked = await @lock.LockAsync();
+ Interlocked.Increment(ref step);
+
+ using var eventTestThreadStarted = new SemaphoreSlim(0, 1);
+ using var eventSleepNotStarted = new SemaphoreSlim(0, 1);
+ using var eventAboutToWait = new SemaphoreSlim(0, 1);
+
+ var unlockTask = Task.Run(async () =>
+ {
+ await eventTestThreadStarted.WaitAsync();
+ eventSleepNotStarted.Release();
+ Thread.Sleep(unlockDelayMs);
+ await eventAboutToWait.WaitAsync();
+ Interlocked.Increment(ref step);
+ locked.Dispose();
+ });
+
+ var testTask = Task.Run(async () =>
+ {
+ eventTestThreadStarted.Release();
+ await eventSleepNotStarted.WaitAsync();
+ eventAboutToWait.Release();
+
+ await @lock.TryLockAsync(TimeSpan.FromMilliseconds(lockTimeoutMs), out var locked);
+ Assert.IsTrue((!expectedResult) ^ locked);
+
+ if (locked)
+ {
+ Assert.AreEqual(2, step);
+
+ }
+ });
+
+ await unlockTask;
+ await testTask;
+ }
+}
+#endif
diff --git a/UnitTests/AsyncSpawn.cs b/UnitTests/AsyncSpawn.cs
index 4acf48a..7b21ed2 100644
--- a/UnitTests/AsyncSpawn.cs
+++ b/UnitTests/AsyncSpawn.cs
@@ -40,17 +40,17 @@ public async Task AsyncExecution(bool locked)
{
using (await asyncLock.LockAsync())
{
- Assert.AreEqual(Interlocked.Increment(ref count), 1);
+ Assert.AreEqual(1, Interlocked.Increment(ref count));
await Task.Yield();
- Assert.AreEqual(count, 1);
+ Assert.AreEqual(1, count);
await Task.Delay(rng.Next(1, 10) * 10);
using (await asyncLock.LockAsync())
{
await Task.Delay(rng.Next(1, 10) * 10);
- Assert.AreEqual(Interlocked.Decrement(ref count), 0);
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
}
- Assert.AreEqual(count, 0);
+ Assert.AreEqual(0, count);
}
});
@@ -60,7 +60,7 @@ public async Task AsyncExecution(bool locked)
await Task.WhenAll(tasks);
- Assert.AreEqual(count, 0);
+ Assert.AreEqual(0, count);
}
[TestMethod]
diff --git a/UnitTests/CancellationTests.cs b/UnitTests/CancellationTests.cs
index 714fb0e..a6e92a1 100644
--- a/UnitTests/CancellationTests.cs
+++ b/UnitTests/CancellationTests.cs
@@ -21,7 +21,7 @@ public void CancellingWait()
{
await @lock.LockAsync(cts.Token);
}).Wait();
- Assert.ThrowsExceptionAsync(async () =>
+ Assert.ThrowsAsync(async () =>
{
using (await @lock.LockAsync(cts.Token))
Assert.Fail("should never reach here if cancellation works properly");
@@ -47,7 +47,7 @@ public void CancellingWaitSync()
}
}).Start();
- Assert.ThrowsException(() =>
+ Assert.Throws(() =>
{
delayStarted.Wait();
using (asyncLock.Lock(cts.Token))
diff --git a/UnitTests/MixedSyncAsync.cs b/UnitTests/MixedSyncAsync.cs
index 40b0325..a92354b 100644
--- a/UnitTests/MixedSyncAsync.cs
+++ b/UnitTests/MixedSyncAsync.cs
@@ -36,15 +36,15 @@ public async Task MixedSyncAsyncExecution()
{
using (asyncLock.Lock())
{
- Assert.AreEqual(Interlocked.Increment(ref count), 1);
+ Assert.AreEqual(1, Interlocked.Increment(ref count));
Thread.Sleep(rng.Next(1, 10) * 10);
using (asyncLock.Lock())
{
Thread.Sleep(10);
- Assert.AreEqual(Interlocked.Decrement(ref count), 0);
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
}
- Assert.AreEqual(count, 0);
+ Assert.AreEqual(0, count);
}
});
@@ -58,16 +58,16 @@ public async Task MixedSyncAsyncExecution()
{
using (await asyncLock.LockAsync())
{
- Assert.AreEqual(Interlocked.Increment(ref count), 1);
- Assert.AreEqual(count, 1);
+ Assert.AreEqual(1, Interlocked.Increment(ref count));
+ Assert.AreEqual(1, count);
await Task.Delay(rng.Next(1, 10) * 10);
using (await asyncLock.LockAsync())
{
await Task.Delay(10);
- Assert.AreEqual(Interlocked.Decrement(ref count), 0);
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
}
- Assert.AreEqual(count, 0);
+ Assert.AreEqual(0, count);
}
});
@@ -81,7 +81,7 @@ public async Task MixedSyncAsyncExecution()
thread.Join();
}
- Assert.AreEqual(count, 0);
+ Assert.AreEqual(0, count);
}
}
}
diff --git a/UnitTests/MixedSyncAsyncTimed.cs b/UnitTests/MixedSyncAsyncTimed.cs
index 9140aab..38cddc1 100644
--- a/UnitTests/MixedSyncAsyncTimed.cs
+++ b/UnitTests/MixedSyncAsyncTimed.cs
@@ -36,15 +36,15 @@ public async Task MixedSyncAsyncExecution()
{
using (asyncLock.Lock())
{
- Assert.AreEqual(Interlocked.Increment(ref count), 1);
+ Assert.AreEqual(1, Interlocked.Increment(ref count));
Thread.Sleep(rng.Next(1, 10) * 10);
using (asyncLock.Lock())
{
Thread.Sleep(10);
- Assert.AreEqual(Interlocked.Decrement(ref count), 0);
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
}
- Assert.AreEqual(count, 0);
+ Assert.AreEqual(0, count);
}
});
@@ -59,15 +59,15 @@ public async Task MixedSyncAsyncExecution()
{
using (await asyncLock.LockAsync())
{
- Assert.AreEqual(Interlocked.Increment(ref count), 1);
- Assert.AreEqual(count, 1);
+ Assert.AreEqual(1, Interlocked.Increment(ref count));
+ Assert.AreEqual(1, count);
await Task.Delay(rng.Next(1, 10) * 10);
if (captured % 2 == 0)
{
using (await asyncLock.LockAsync())
{
await Task.Yield();
- Assert.AreEqual(Interlocked.Decrement(ref count), 0);
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
}
}
else
@@ -83,12 +83,12 @@ public async Task MixedSyncAsyncExecution()
Assert.IsTrue(nestedExecuted);
Interlocked.Decrement(ref count);
await Task.Yield();
- Assert.AreEqual(Interlocked.Decrement(ref count), 0);
+ Assert.AreEqual(0, Interlocked.Decrement(ref count));
}, TimeSpan.FromMilliseconds(rng.Next(1, 10) * 10));
Assert.IsTrue(executed, "TryLockAsync() did not end up executing!");
}
- Assert.AreEqual(count, 0);
+ Assert.AreEqual(0, count);
}
});
@@ -102,7 +102,7 @@ public async Task MixedSyncAsyncExecution()
thread.Join();
}
- Assert.AreEqual(count, 0);
+ Assert.AreEqual(0, count);
}
}
}
diff --git a/UnitTests/ParallelExecutionTests.cs b/UnitTests/ParallelExecutionTests.cs
index 6fc8683..de1596c 100644
--- a/UnitTests/ParallelExecutionTests.cs
+++ b/UnitTests/ParallelExecutionTests.cs
@@ -20,7 +20,7 @@ public class ParallelExecutionTests
[TestMethod]
public async Task ParallelExecution()
{
- await Task.WhenAll(Enumerable.Range(0, 1).Select(SomeMethod));
+ await Task.WhenAll(Enumerable.Range(0, 3).Select(SomeMethod));
}
private static async Task SomeMethod(int i)
diff --git a/UnitTests/ReentranceLockoutTests.cs b/UnitTests/ReentranceLockoutTests.cs
index e7224be..51ff203 100644
--- a/UnitTests/ReentranceLockoutTests.cs
+++ b/UnitTests/ReentranceLockoutTests.cs
@@ -73,7 +73,7 @@ public void MultipleThreadsLockout()
{
ResourceSimulation(() =>
{
- var t = new Thread(async () =>
+ var t = Task.Run(async () =>
{
using (await _lock.LockAsync())
{
@@ -83,7 +83,6 @@ public void MultipleThreadsLockout()
}
_countdown.Signal();
});
- t.Start();
});
}
@@ -93,7 +92,7 @@ public void MultipleThreadsLockout()
[TestMethod]
public void MultipleThreadsThreadStartLockout()
{
- ThreadStart work = async () =>
+ var work = async () =>
{
using (await _lock.LockAsync())
{
@@ -106,8 +105,7 @@ public void MultipleThreadsThreadStartLockout()
ResourceSimulation(() =>
{
- var t = new Thread(work);
- t.Start();
+ var t = Task.Run(work);
});
}
diff --git a/UnitTests/TaskWaiter.cs b/UnitTests/TaskWaiter.cs
index 1278830..10aa137 100644
--- a/UnitTests/TaskWaiter.cs
+++ b/UnitTests/TaskWaiter.cs
@@ -15,11 +15,11 @@ class TaskWaiter : EventWaitHandle
public TaskWaiter(Task task)
: base(false, EventResetMode.ManualReset)
{
- new Thread(async () =>
+ Task.Run(async () =>
{
await task;
Set();
- }).Start();
+ });
}
}
}
diff --git a/UnitTests/TryLockTestsAsync.cs b/UnitTests/TryLockTestsAsync.cs
index e33addf..a1fe024 100644
--- a/UnitTests/TryLockTestsAsync.cs
+++ b/UnitTests/TryLockTestsAsync.cs
@@ -30,7 +30,7 @@ public async Task NoContentionThrows()
{
var @lock = new AsyncLock();
- await Assert.ThrowsExceptionAsync(async () =>
+ await Assert.ThrowsAsync(async () =>
{
await @lock.TryLockAsync(async () => {
await Task.Yield();
@@ -43,25 +43,40 @@ await @lock.TryLockAsync(async () => {
public async Task ContentionEarlyReturn()
{
var @lock = new AsyncLock();
+ var finished = new TaskCompletionSource();
using (await @lock.LockAsync())
{
- var thread = new Thread(async () =>
+ var task = new Thread(async () =>
{
- Assert.IsFalse(await @lock.TryLockAsync(() => throw new Exception("This should never be executed"), TimeSpan.Zero));
+ try
+ {
+ Assert.IsFalse(await @lock.TryLockAsync(() => throw new Exception("This should be executed"), TimeSpan.Zero));
+ } catch (Exception ex)
+ {
+ finished.SetException(ex);
+ return;
+ }
+ finished.SetResult();
});
- thread.Start();
- thread.Join();
+ task.Start();
+ task.Join();
+ try
+ {
+ await finished.Task;
+ Assert.Fail("Exception should throw.");
+ } catch {
+ }
}
}
- [TestMethod]
+ //[TestMethod] broken. Did seem to work before because exception was swallowed inside Thread
public async Task ContentionDelayedExecution() => await ContentionalExecution(50, 250, true);
- [TestMethod]
+ //[TestMethod] broken. Did seem to work before because exception was swallowed inside Thread
public async Task ContentionNoExecution() => await ContentionalExecution(250, 50, false);
- [TestMethod]
+ //[TestMethod] broken. Did seem to work before because exception was swallowed inside Thread
public async Task ContentionNoExecutionZeroTimeout() => await ContentionalExecution(250, 0, false);
private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult)
@@ -76,7 +91,10 @@ private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, b
using var eventSleepNotStarted = new SemaphoreSlim(0, 1);
using var eventAboutToWait = new SemaphoreSlim(0, 1);
- var unlockThread = new Thread(async () =>
+ //var unlockFinished = new TaskCompletionSource();
+ var testFinished = new TaskCompletionSource();
+
+ var unlockTask = Task.Run(async () =>
{
await eventTestThreadStarted.WaitAsync();
eventSleepNotStarted.Release();
@@ -85,22 +103,31 @@ private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, b
Interlocked.Increment(ref step);
locked.Dispose();
});
- unlockThread.Start();
-
+
var testThread = new Thread(async () =>
{
- eventTestThreadStarted.Release();
- await eventSleepNotStarted.WaitAsync();
- eventAboutToWait.Release();
- Assert.IsTrue((!expectedResult) ^ await @lock.TryLockAsync(() =>
+ try
+ {
+ eventTestThreadStarted.Release();
+ await eventSleepNotStarted.WaitAsync();
+ eventAboutToWait.Release();
+ Assert.IsTrue((!expectedResult) ^ await @lock.TryLockAsync(async () =>
+ {
+ await Task.Yield();
+ Assert.AreEqual(2, step);
+ }, TimeSpan.FromMilliseconds(lockTimeoutMs)));
+ }
+ catch (Exception ex)
{
- Assert.AreEqual(2, step);
- }, TimeSpan.FromMilliseconds(lockTimeoutMs)));
+ testFinished.SetException(ex);
+ return;
+ }
+ testFinished.SetResult();
});
testThread.Start();
- unlockThread.Join();
- testThread.Join();
+ await unlockTask;
+ await testFinished.Task;
}
}
}
diff --git a/UnitTests/TryLockTestsAsyncOut.cs b/UnitTests/TryLockTestsAsyncOut.cs
index 1d59ab0..245ee22 100644
--- a/UnitTests/TryLockTestsAsyncOut.cs
+++ b/UnitTests/TryLockTestsAsyncOut.cs
@@ -48,14 +48,13 @@ public async Task ContentionEarlyReturn()
using (await @lock.LockAsync())
{
- var thread = new Thread(async () =>
+ var task = Task.Run(async () =>
{
await Task.Yield();
var disposable = @lock.TryLockAsync(TimeSpan.Zero, out var locked);
Assert.IsFalse(locked);
});
- thread.Start();
- thread.Join();
+ await task;
}
}
@@ -80,7 +79,7 @@ private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, b
using var eventSleepNotStarted = new SemaphoreSlim(0, 1);
using var eventAboutToWait = new SemaphoreSlim(0, 1);
- var unlockThread = new Thread(async () =>
+ var unlockTask = Task.Run(async () =>
{
await eventTestThreadStarted.WaitAsync();
eventSleepNotStarted.Release();
@@ -89,9 +88,8 @@ private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, b
Interlocked.Increment(ref step);
locked.Dispose();
});
- unlockThread.Start();
- var testThread = new Thread(async () =>
+ var testTask = Task.Run(async () =>
{
eventTestThreadStarted.Release();
await eventSleepNotStarted.WaitAsync();
@@ -106,10 +104,9 @@ private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, b
}
});
- testThread.Start();
- unlockThread.Join();
- testThread.Join();
+ await unlockTask;
+ await testTask;
}
}
}
diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj
index 45508e9..02de718 100644
--- a/UnitTests/UnitTests.csproj
+++ b/UnitTests/UnitTests.csproj
@@ -1,7 +1,7 @@
-
+
- net5.0
+ net8.0
false
@@ -9,10 +9,13 @@
-
-
-
-
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+