diff --git a/AsyncLock/AsyncLock.cs b/AsyncLock/AsyncLock.cs index 0d922aa..a9cc28b 100644 --- a/AsyncLock/AsyncLock.cs +++ b/AsyncLock/AsyncLock.cs @@ -1,502 +1,477 @@ using System; -using System.Diagnostics; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; -namespace NeoSmart.AsyncLock +namespace NeoSmart.AsyncLock; + +public class AsyncLock { - public class AsyncLock + private readonly SemaphoreSlim _reentrancy = new(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. + private readonly SemaphoreSlim _retry = new(0, 1); + private const long UnlockedId = 0; // "owning" task id when unlocked + private long _owningId = UnlockedId; + private 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 + // 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(); + private static long AsyncId => _asyncId.Value; + private static int ThreadId => Environment.CurrentManagedThreadId; + + public AsyncLock() + { + } + + private readonly struct InnerLock : 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; - // 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 AsyncLock() + private readonly AsyncLock _parent; + private readonly long _oldId; + private readonly int _oldThreadId; + + internal InnerLock(AsyncLock parent, long oldId, int oldThreadId) { + _parent = parent; + _oldId = oldId; + _oldThreadId = oldThreadId; } -#if !DEBUG - readonly -#endif - struct InnerLock : IDisposable + internal async Task ObtainLockAsync(CancellationToken ct = default) { - private readonly AsyncLock _parent; - private readonly long _oldId; - private readonly int _oldThreadId; -#if DEBUG - private bool _disposed; -#endif - - internal InnerLock(AsyncLock parent, long oldId, int oldThreadId) + while (true) { - _parent = parent; - _oldId = oldId; - _oldThreadId = oldThreadId; -#if DEBUG - _disposed = false; -#endif + await _parent._reentrancy.WaitAsync(ct).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(ct).ConfigureAwait(false); + _parent._reentrancy.Release(); + await waitTask; } + // 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; + } - internal async Task ObtainLockAsync(CancellationToken ct = default) + internal async Task TryObtainLockAsync(TimeSpan timeout) + { + // In case of zero-timeout, don't even wait for protective lock contention + if (timeout == TimeSpan.Zero) { - while (true) + _parent._reentrancy.Wait(timeout); + if (InnerTryEnter(synchronous: false)) { - await _parent._reentrancy.WaitAsync(ct).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(ct).ConfigureAwait(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. + _parent._owningThreadId = ThreadId; _parent._reentrancy.Release(); - await waitTask; + return this; } - // 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; + return null; } - internal async Task TryObtainLockAsync(TimeSpan timeout) + 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) { - // In case of zero-timeout, don't even wait for protective lock contention - if (timeout == TimeSpan.Zero) + await _parent._reentrancy.WaitAsync(remainder).ConfigureAwait(false); + if (InnerTryEnter(synchronous: false)) { - _parent._reentrancy.Wait(timeout); - 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. - _parent._owningThreadId = ThreadId; - _parent._reentrancy.Release(); - return this; - } + // 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 null; + return this; } + _parent._reentrancy.Release(); - 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) + now = DateTimeOffset.UtcNow; + remainder -= now - last; + last = now; + if (remainder < TimeSpan.Zero) { - await _parent._reentrancy.WaitAsync(remainder).ConfigureAwait(false); - 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. - _parent._owningThreadId = ThreadId; - _parent._reentrancy.Release(); - return this; - } - _parent._reentrancy.Release(); - - 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; } - return null; - } - - internal async Task TryObtainLockAsync(CancellationToken cancel) - { - try - { - while (true) - { - await _parent._reentrancy.WaitAsync(cancel).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(cancel).ConfigureAwait(false); - _parent._reentrancy.Release(); - await waitTask; - } - } - catch (OperationCanceledException) + var waitTask = _parent._retry.WaitAsync(remainder).ConfigureAwait(false); + _parent._reentrancy.Release(); + if (!await waitTask) { return null; } - // 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; + now = DateTimeOffset.UtcNow; + remainder -= now - last; + last = now; } - internal IDisposable ObtainLock(CancellationToken cancellationToken) + return null; + } + + internal async Task TryObtainLockAsync(CancellationToken cancel) + { + try { while (true) { - _parent._reentrancy.Wait(cancellationToken); - if (InnerTryEnter(synchronous: true)) + await _parent._reentrancy.WaitAsync(cancel).ConfigureAwait(false); + if (InnerTryEnter(synchronous: false)) { - _parent._reentrancy.Release(); break; } // We need to wait for someone to leave the lock before trying again. - var waitTask = _parent._retry.WaitAsync(cancellationToken); + var waitTask = _parent._retry.WaitAsync(cancel).ConfigureAwait(false); _parent._reentrancy.Release(); - // This should be safe since the task we are awaiting doesn't need to make progress - // itself to complete - it will be completed by another thread altogether. cf SemaphoreSlim internals. - waitTask.GetAwaiter().GetResult(); + await waitTask; } - return this; } + catch (OperationCanceledException) + { + return null; + } + + // 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; + } - internal IDisposable? TryObtainLock(TimeSpan timeout) + internal IDisposable ObtainLock(CancellationToken cancellationToken) + { + while (true) { - // In case of zero-timeout, don't even wait for protective lock contention - if (timeout == TimeSpan.Zero) + _parent._reentrancy.Wait(cancellationToken); + if (InnerTryEnter(synchronous: true)) { - _parent._reentrancy.Wait(timeout); - if (InnerTryEnter(synchronous: true)) - { - _parent._reentrancy.Release(); - return this; - } _parent._reentrancy.Release(); - return null; + break; } - - 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) + var waitTask = _parent._retry.WaitAsync(cancellationToken); + _parent._reentrancy.Release(); + // This should be safe since the task we are awaiting doesn't need to make progress + // itself to complete - it will be completed by another thread altogether. cf SemaphoreSlim internals. + waitTask.GetAwaiter().GetResult(); + } + return this; + } + + internal IDisposable? TryObtainLock(TimeSpan timeout) + { + // In case of zero-timeout, don't even wait for protective lock contention + if (timeout == TimeSpan.Zero) + { + _parent._reentrancy.Wait(timeout); + if (InnerTryEnter(synchronous: true)) { - _parent._reentrancy.Wait(remainder); - if (InnerTryEnter(synchronous: true)) - { - _parent._reentrancy.Release(); - return this; - } + _parent._reentrancy.Release(); + return this; + } + _parent._reentrancy.Release(); + return null; + } - now = DateTimeOffset.UtcNow; - remainder -= now - last; - last = now; + var now = DateTimeOffset.UtcNow; + var last = now; + var remainder = timeout; - var waitTask = _parent._retry.WaitAsync(remainder); + // We need to wait for someone to leave the lock before trying again. + while (remainder > TimeSpan.Zero) + { + _parent._reentrancy.Wait(remainder); + if (InnerTryEnter(synchronous: true)) + { _parent._reentrancy.Release(); - if (!waitTask.GetAwaiter().GetResult()) - { - return null; - } + return this; + } + + now = DateTimeOffset.UtcNow; + remainder -= now - last; + last = now; - now = DateTimeOffset.UtcNow; - remainder -= now - last; - last = now; + var waitTask = _parent._retry.WaitAsync(remainder); + _parent._reentrancy.Release(); + if (!waitTask.GetAwaiter().GetResult()) + { + return null; } - return null; + now = DateTimeOffset.UtcNow; + remainder -= now - last; + last = now; } - private bool InnerTryEnter(bool synchronous = false) + return null; + } + + private bool InnerTryEnter(bool synchronous = false) + { + if (synchronous) { - bool result = false; - if (synchronous) + if (_parent._owningThreadId == UnlockedId) + { + _parent._owningThreadId = ThreadId; + } + else if (_parent._owningThreadId != ThreadId) + { + return false; + } + _parent._owningId = AsyncLock.AsyncId; + } + else + { + if (_parent._owningId == UnlockedId) { - if (_parent._owningThreadId == UnlockedId) - { - _parent._owningThreadId = ThreadId; - } - else if (_parent._owningThreadId != ThreadId) - { - return false; - } _parent._owningId = AsyncLock.AsyncId; } + else if (_parent._owningId != _oldId) + { + // Another thread currently owns the lock + return false; + } else { - if (_parent._owningId == UnlockedId) - { - _parent._owningId = AsyncLock.AsyncId; - } - else if (_parent._owningId != _oldId) - { - // Another thread currently owns the lock - return false; - } - else - { - // Nested re-entrance - _parent._owningId = AsyncId; - } + // Nested re-entrance + _parent._owningId = AsyncId; } - - // We can go in - _parent._reentrances += 1; - result = true; - return result; } - public void Dispose() + // We can go in + _parent._reentrances++; + return true; + } + + public void Dispose() + { + var oldId = this._oldId; + var oldThreadId = this._oldThreadId; + _parent._reentrancy.Wait(); + try { -#if DEBUG - Debug.Assert(!_disposed); - _disposed = true; -#endif - var @this = this; - var oldId = this._oldId; - var oldThreadId = this._oldThreadId; - @this._parent._reentrancy.Wait(); - try + _parent._reentrances--; + _parent._owningId = oldId; + _parent._owningThreadId = oldThreadId; + if (_parent._reentrances == 0) { - @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; - } - // 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(); - } + // 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. + _parent._owningId = UnlockedId; + _parent._owningThreadId = (int)UnlockedId; } - finally + // 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 (_parent._retry.CurrentCount == 0) { - @this._parent._reentrancy.Release(); + _parent._retry.Release(); } } + finally + { + _parent._reentrancy.Release(); + } } + } - // Make sure InnerLock.LockAsync() does not use await, because an async function triggers a snapshot of - // the AsyncLocal value. - public Task LockAsync(CancellationToken ct = default) - { - var @lock = new InnerLock(this, _asyncId.Value, ThreadId); - _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter); - return @lock.ObtainLockAsync(ct); - } + // Make sure InnerLock.LockAsync() does not use await, because an async function triggers a snapshot of + // the AsyncLocal value. + public Task LockAsync(CancellationToken ct = default) + { + var @lock = new InnerLock(this, _asyncId.Value, ThreadId); + _asyncId.Value = Interlocked.Increment(ref AsyncStackCounter); + return @lock.ObtainLockAsync(ct); + } - // 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 AsyncLock.AsyncStackCounter); + // 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 AsyncStackCounter); - return @lock.TryObtainLockAsync(timeout) - .ContinueWith(state => + return @lock.TryObtainLockAsync(timeout) + .ContinueWith(state => + { + if (state.Exception is AggregateException ex) { - if (state.Exception is AggregateException ex) - { - ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); - } - var disposableLock = state.Result; - if (disposableLock is null) - { - return false; - } + ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); + } + var disposableLock = state.Result; + if (disposableLock is null) + { + return false; + } - try - { - callback(); - } - finally - { - disposableLock.Dispose(); - } - return true; - }); - } + 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 AsyncLock.AsyncStackCounter); + // 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 AsyncStackCounter); - return @lock.TryObtainLockAsync(timeout) - .ContinueWith(state => + return @lock.TryObtainLockAsync(timeout) + .ContinueWith(state => + { + if (state.Exception is AggregateException ex) { - if (state.Exception is AggregateException ex) - { - ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); - } - var disposableLock = state.Result; - if (disposableLock is null) + ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); + } + var disposableLock = state.Result; + if (disposableLock is null) + { + return Task.FromResult(false); + } + + return callback() + .ContinueWith(result => { - return Task.FromResult(false); - } + disposableLock.Dispose(); - return callback() - .ContinueWith(result => + if (result.Exception is AggregateException ex) { - disposableLock.Dispose(); - - if (result.Exception is AggregateException ex) - { - ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); - } + ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); + } - return true; - }, TaskScheduler.Default); - }, TaskScheduler.Default).Unwrap(); - } + 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 cancel) - { - var @lock = new InnerLock(this, _asyncId.Value, ThreadId); - _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter); + // 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 cancel) + { + var @lock = new InnerLock(this, _asyncId.Value, ThreadId); + _asyncId.Value = Interlocked.Increment(ref AsyncStackCounter); - return @lock.TryObtainLockAsync(cancel) - .ContinueWith(state => + return @lock.TryObtainLockAsync(cancel) + .ContinueWith(state => + { + if (state.Exception is AggregateException ex) { - if (state.Exception is AggregateException ex) - { - ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); - } - var disposableLock = state.Result; - if (disposableLock is null) - { - return false; - } + ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); + } + var disposableLock = state.Result; + if (disposableLock is null) + { + return false; + } - try - { - callback(); - } - finally - { - disposableLock.Dispose(); - } - return true; - }, TaskScheduler.Default); - } + 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 cancel) - { - var @lock = new InnerLock(this, _asyncId.Value, ThreadId); - _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter); + // 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 cancel) + { + var @lock = new InnerLock(this, _asyncId.Value, ThreadId); + _asyncId.Value = Interlocked.Increment(ref AsyncStackCounter); - return @lock.TryObtainLockAsync(cancel) - .ContinueWith(state => + return @lock.TryObtainLockAsync(cancel) + .ContinueWith(state => + { + if (state.Exception is AggregateException ex) { - if (state.Exception is AggregateException ex) - { - ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); - } - var disposableLock = state.Result; - if (disposableLock is null) + ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); + } + var disposableLock = state.Result; + if (disposableLock is null) + { + return Task.FromResult(false); + } + + return callback() + .ContinueWith(result => { - return Task.FromResult(false); - } + disposableLock.Dispose(); - return callback() - .ContinueWith(result => + if (result.Exception is AggregateException ex) { - disposableLock.Dispose(); + ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); + } - if (result.Exception is AggregateException ex) - { - ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); - } + return true; + }, TaskScheduler.Default); + }, TaskScheduler.Default).Unwrap(); + } - 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 AsyncStackCounter); + return @lock.ObtainLock(cancellationToken); + } - public IDisposable Lock(CancellationToken cancellationToken = default) + 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 AsyncStackCounter); + var lockDisposable = @lock.TryObtainLock(timeout); + if (lockDisposable is null) { - 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); - return @lock.ObtainLock(cancellationToken); + return false; } - public bool TryLock(Action callback, TimeSpan timeout) + // Execute the callback then release the lock + try { - 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) - { - return false; - } - - // Execute the callback then release the lock - try - { - callback(); - } - finally - { - lockDisposable.Dispose(); - } - return true; + callback(); + } + finally + { + lockDisposable.Dispose(); } + return true; } } diff --git a/AsyncLock/AsyncLock.csproj b/AsyncLock/AsyncLock.csproj index 66bc0f2..fc148a7 100644 --- a/AsyncLock/AsyncLock.csproj +++ b/AsyncLock/AsyncLock.csproj @@ -1,7 +1,8 @@  - netstandard1.3;netstandard2.1 + netstandard2.0;netstandard2.1;net8.0;net9.0 + latest NeoSmart.AsyncLock NeoSmart.AsyncLock True @@ -30,7 +31,6 @@ - true True AsyncLock.snk README.md diff --git a/AsyncLock/NullDisposable.cs b/AsyncLock/NullDisposable.cs deleted file mode 100644 index 824963f..0000000 --- a/AsyncLock/NullDisposable.cs +++ /dev/null @@ -1,15 +0,0 @@ -#if TRY_LOCK_OUT_BOOL -using System; -using System.Collections.Generic; -using System.Text; - -namespace NeoSmart.AsyncLock -{ - sealed class NullDisposable : IDisposable - { - public void Dispose() - { - } - } -} -#endif diff --git a/UnitTests/AsyncIdTests.cs b/UnitTests/AsyncIdTests.cs index 08b2b67..662b921 100644 --- a/UnitTests/AsyncIdTests.cs +++ b/UnitTests/AsyncIdTests.cs @@ -1,82 +1,78 @@ 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; #if false -namespace AsyncLockTests +namespace AsyncLockTests; + +[TestClass] +public class AsyncIdTests { - [TestClass] - public class AsyncIdTests + [TestMethod] + public void TaskIdUniqueness() { - [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); + 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) + for (int i = 0; i < testCount; ++i) + { + Task.Run(async () => { - Task.Run(async () => + lock (threadIds) { - lock (threadIds) + if (!threadIds.Add(AsyncLock.ThreadId)) { - if (!threadIds.Add(AsyncLock.ThreadId)) - { - failure.Set(); - } + 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(); + } + countdown.Signal(); + await abort.WaitAsync(); + }); } - public void ThreadIdUniqueness() + if (WaitHandle.WaitAny([countdown.WaitHandle, failure.WaitHandle]) == 1) { - var testCount = 100; - var countdown = new CountdownEvent(testCount); - var failure = new ManualResetEventSlim(false); - var threadIds = new SortedSet(); - var abort = new SemaphoreSlim(0, 1); + Assert.Fail("A duplicate thread id was found!"); + } + + abort.Release(); + } - for (int i = 0; i < testCount; ++i) + 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 () => { - Task.Run(async () => + lock (threadIds) { - lock (threadIds) + if (!threadIds.Add(AsyncLock.ThreadId)) { - if (!threadIds.Add(AsyncLock.ThreadId)) - { - failure.Set(); - } + failure.Set(); } - countdown.Signal(); - await abort.WaitAsync(); - }); - } - - if (WaitHandle.WaitAny(new[] { countdown.WaitHandle, failure.WaitHandle }) == 1) - { - Assert.Fail("A duplicate thread id was found!"); - } + } + countdown.Signal(); + await abort.WaitAsync(); + }); + } - abort.Release(); + if (WaitHandle.WaitAny([countdown.WaitHandle, failure.WaitHandle]) == 1) + { + Assert.Fail("A duplicate thread id was found!"); } + + abort.Release(); } } #endif diff --git a/UnitTests/AsyncSpawn.cs b/UnitTests/AsyncSpawn.cs index 4acf48a..1a84096 100644 --- a/UnitTests/AsyncSpawn.cs +++ b/UnitTests/AsyncSpawn.cs @@ -2,77 +2,75 @@ using NeoSmart.AsyncLock; using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace AsyncLockTests +namespace AsyncLockTests; + +/// +/// 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 { - /// - /// 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 readonly struct NullDisposable : IDisposable - { - public void Dispose() { } - } + public void Dispose() { } + } + + public async Task AsyncExecution(bool locked) + { + var count = 0; + var tasks = new List(70); + var asyncLock = new AsyncLock(); + var rng = new Random(); - public async Task AsyncExecution(bool locked) { - var count = 0; - var tasks = new List(70); - var asyncLock = new AsyncLock(); - var rng = new Random(); + using var l = locked ? await asyncLock.LockAsync() : new NullDisposable(); + for (int i = 0; i < 10; ++i) { - using var l = locked ? await asyncLock.LockAsync() : new NullDisposable(); - - for (int i = 0; i < 10; ++i) + var task = Task.Run(async () => { - 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()) { - Assert.AreEqual(Interlocked.Increment(ref count), 1); - await Task.Yield(); - Assert.AreEqual(count, 1); 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(count, 0); + Assert.AreEqual(0, Interlocked.Decrement(ref count)); } - }); - tasks.Add(task); - } + Assert.AreEqual(0, count); + } + + }); + tasks.Add(task); } + } - await Task.WhenAll(tasks); + await Task.WhenAll(tasks); - Assert.AreEqual(count, 0); - } + Assert.AreEqual(0, count); + } - [TestMethod] - public async Task AsyncExecutionLocked() - { - await AsyncExecution(true); - } + [TestMethod] + public async Task AsyncExecutionLocked() + { + await AsyncExecution(true); + } - [TestMethod] - public async Task AsyncExecutionUnlocked() - { - await AsyncExecution(false); - } + [TestMethod] + public async Task AsyncExecutionUnlocked() + { + await AsyncExecution(false); } } diff --git a/UnitTests/CancellationTests.cs b/UnitTests/CancellationTests.cs index 714fb0e..aac5ca2 100644 --- a/UnitTests/CancellationTests.cs +++ b/UnitTests/CancellationTests.cs @@ -1,69 +1,65 @@ 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; -namespace AsyncLockTests +namespace AsyncLockTests; + +[TestClass] +public class CancellationTests { - [TestClass] - public class CancellationTests + [TestMethod] + public void CancellingWait() { - [TestMethod] - public void CancellingWait() + var @lock = new AsyncLock(); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Task.Run(async () => { - var @lock = new AsyncLock(); - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); - Task.Run(async () => - { - await @lock.LockAsync(cts.Token); - }).Wait(); - Assert.ThrowsExceptionAsync(async () => - { - using (await @lock.LockAsync(cts.Token)) - Assert.Fail("should never reach here if cancellation works properly"); - }).Wait(); - - } - - [TestMethod] - public void CancellingWaitSync() + await @lock.LockAsync(cts.Token); + }).Wait(); + Assert.ThrowsExactlyAsync(async () => { - var asyncLock = new AsyncLock(); - var cts = new CancellationTokenSource(250); - var delayStarted = new ManualResetEventSlim(false); - var waiter1Finished = new SemaphoreSlim(0, 1); + using (await @lock.LockAsync(cts.Token)) + Assert.Fail("should never reach here if cancellation works properly"); + }).Wait(); - new Thread(() => - { - using (asyncLock.Lock(cts.Token)) - { - // hold the lock until our later attempt is called - delayStarted.Set(); - waiter1Finished.Wait(); - } - }).Start(); + } - Assert.ThrowsException(() => + [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)) { - delayStarted.Wait(); - using (asyncLock.Lock(cts.Token)) - { - Assert.Fail("should never reach here if cancellation works properly."); - } - }); - waiter1Finished.Release(1); + // hold the lock until our later attempt is called + delayStarted.Set(); + waiter1Finished.Wait(); + } + }).Start(); - // 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)) + Assert.ThrowsExactly(() => + { + delayStarted.Wait(); + using (asyncLock.Lock(cts.Token)) { - // reaching this line means the test passed - // a OperationCanceledException will indicate test failure + 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/LimitedResource.cs b/UnitTests/LimitedResource.cs index ed25660..81e3b10 100644 --- a/UnitTests/LimitedResource.cs +++ b/UnitTests/LimitedResource.cs @@ -1,38 +1,33 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; -using System.Threading.Tasks; -namespace AsyncLockTests +namespace AsyncLockTests; + +/// +/// A fake resource that will invoke a callback if more than n instances are simultaneously accessed +/// +class LimitedResource { - /// - /// A fake resource that will invoke a callback if more than n instances are simultaneously accessed - /// - class LimitedResource - { - private readonly int _max = 1; - private int _unsafe = 0; - private readonly Action _failureCallback; + private readonly int _max = 1; + private int _unsafe = 0; + private readonly Action _failureCallback; - public LimitedResource(Action onFailure, int maxSimultaneous = 1) - { - _max = maxSimultaneous; - _failureCallback = onFailure; - } + public LimitedResource(Action onFailure, int maxSimultaneous = 1) + { + _max = maxSimultaneous; + _failureCallback = onFailure; + } - public void BeginSomethingDangerous() + public void BeginSomethingDangerous() + { + if (Interlocked.Increment(ref _unsafe) > _max) { - if (Interlocked.Increment(ref _unsafe) > _max) - { - _failureCallback(); - } + _failureCallback(); } + } - public void EndSomethingDangerous() - { - Interlocked.Decrement(ref _unsafe); - } + public void EndSomethingDangerous() + { + Interlocked.Decrement(ref _unsafe); } } diff --git a/UnitTests/MixedSyncAsync.cs b/UnitTests/MixedSyncAsync.cs index 40b0325..0a04dfc 100644 --- a/UnitTests/MixedSyncAsync.cs +++ b/UnitTests/MixedSyncAsync.cs @@ -2,86 +2,84 @@ using NeoSmart.AsyncLock; using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace AsyncLockTests +namespace AsyncLockTests; + +/// +/// 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 { - /// - /// 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() { - [TestMethod] - public async Task MixedSyncAsyncExecution() - { - var count = 0; - var threads = new List(10); - var tasks = new List(10); - var asyncLock = new AsyncLock(); - var rng = new Random(); + var count = 0; + var threads = new List(10); + var tasks = new List(10); + var asyncLock = new AsyncLock(); + var rng = new Random(); + { + using var l = asyncLock.Lock(); + for (int i = 0; i < 10; ++i) { - using var l = asyncLock.Lock(); - for (int i = 0; i < 10; ++i) + var thread = new Thread(() => { - var thread = new Thread(() => + using (asyncLock.Lock()) { + Assert.AreEqual(1, Interlocked.Increment(ref count)); + Thread.Sleep(rng.Next(1, 10) * 10); using (asyncLock.Lock()) { - Assert.AreEqual(Interlocked.Increment(ref count), 1); - Thread.Sleep(rng.Next(1, 10) * 10); - using (asyncLock.Lock()) - { - Thread.Sleep(10); - Assert.AreEqual(Interlocked.Decrement(ref count), 0); - } - - Assert.AreEqual(count, 0); + Thread.Sleep(10); + Assert.AreEqual(0, Interlocked.Decrement(ref count)); } - }); - thread.Start(); - threads.Add(thread); - } + Assert.AreEqual(0, count); + } - for (int i = 0; i < 10; ++i) + }); + thread.Start(); + threads.Add(thread); + } + + for (int i = 0; i < 10; ++i) + { + var task = Task.Run(async () => { - 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); using (await asyncLock.LockAsync()) { - Assert.AreEqual(Interlocked.Increment(ref count), 1); - Assert.AreEqual(count, 1); - 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(count, 0); + await Task.Delay(10); + Assert.AreEqual(0, Interlocked.Decrement(ref count)); } - }); - tasks.Add(task); - } - } + Assert.AreEqual(0, count); + } - await Task.WhenAll(tasks); - foreach (var thread in threads) - { - thread.Join(); + }); + tasks.Add(task); } + } - Assert.AreEqual(count, 0); + await Task.WhenAll(tasks); + foreach (var thread in threads) + { + thread.Join(); } + + Assert.AreEqual(0, count); } } diff --git a/UnitTests/MixedSyncAsyncTimed.cs b/UnitTests/MixedSyncAsyncTimed.cs index 9140aab..f039daf 100644 --- a/UnitTests/MixedSyncAsyncTimed.cs +++ b/UnitTests/MixedSyncAsyncTimed.cs @@ -2,107 +2,105 @@ using NeoSmart.AsyncLock; using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace AsyncLockTests +namespace AsyncLockTests; + +/// +/// 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 { - /// - /// 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() { - [TestMethod] - public async Task MixedSyncAsyncExecution() - { - var count = 0; - var threads = new List(10); - var tasks = new List(10); - var asyncLock = new AsyncLock(); - var rng = new Random(); + var count = 0; + var threads = new List(10); + var tasks = new List(10); + var asyncLock = new AsyncLock(); + var rng = new Random(); + { + using var l = asyncLock.Lock(); + for (int i = 0; i < 10; ++i) { - using var l = asyncLock.Lock(); - for (int i = 0; i < 10; ++i) + var thread = new Thread(() => { - var thread = new Thread(() => + using (asyncLock.Lock()) { + Assert.AreEqual(1, Interlocked.Increment(ref count)); + Thread.Sleep(rng.Next(1, 10) * 10); using (asyncLock.Lock()) { - Assert.AreEqual(Interlocked.Increment(ref count), 1); - Thread.Sleep(rng.Next(1, 10) * 10); - using (asyncLock.Lock()) - { - Thread.Sleep(10); - Assert.AreEqual(Interlocked.Decrement(ref count), 0); - } - - Assert.AreEqual(count, 0); + Thread.Sleep(10); + Assert.AreEqual(0, Interlocked.Decrement(ref count)); } - }); - thread.Start(); - threads.Add(thread); - } + Assert.AreEqual(0, count); + } + + }); + thread.Start(); + threads.Add(thread); + } - for (int i = 0; i < 10; ++i) + for (int i = 0; i < 10; ++i) + { + var captured = i; + var task = Task.Run(async () => { - var captured = i; - var task = Task.Run(async () => + using (await asyncLock.LockAsync()) { - 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) { - Assert.AreEqual(Interlocked.Increment(ref count), 1); - Assert.AreEqual(count, 1); - await Task.Delay(rng.Next(1, 10) * 10); - if (captured % 2 == 0) + using (await asyncLock.LockAsync()) { - using (await asyncLock.LockAsync()) - { - await Task.Yield(); - Assert.AreEqual(Interlocked.Decrement(ref count), 0); - } + await Task.Yield(); + Assert.AreEqual(0, Interlocked.Decrement(ref count)); } - else + } + else + { + var executed = await asyncLock.TryLockAsync(async () => { - var executed = await asyncLock.TryLockAsync(async () => + // Throw in a recursive async lock invocation + bool nestedExecuted = 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(Interlocked.Decrement(ref count), 0); - }, TimeSpan.FromMilliseconds(rng.Next(1, 10) * 10)); - Assert.IsTrue(executed, "TryLockAsync() did not end up executing!"); - } - - Assert.AreEqual(count, 0); + 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!"); } - }); - tasks.Add(task); - } - } + Assert.AreEqual(0, count); + } - await Task.WhenAll(tasks); - foreach (var thread in threads) - { - thread.Join(); + }); + tasks.Add(task); } + } - Assert.AreEqual(count, 0); + await Task.WhenAll(tasks); + foreach (var thread in threads) + { + thread.Join(); } + + Assert.AreEqual(0, count); } } diff --git a/UnitTests/ParallelExecutionTests.cs b/UnitTests/ParallelExecutionTests.cs index 6fc8683..61e02e1 100644 --- a/UnitTests/ParallelExecutionTests.cs +++ b/UnitTests/ParallelExecutionTests.cs @@ -1,42 +1,40 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NeoSmart.AsyncLock; -using System; using System.Linq; using System.Threading.Tasks; -namespace AsyncLockTests +namespace AsyncLockTests; + +/// +/// 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 { - /// - /// 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() { - [TestMethod] - public async Task ParallelExecution() - { - await Task.WhenAll(Enumerable.Range(0, 1).Select(SomeMethod)); - } + await Task.WhenAll(Enumerable.Range(0, 1).Select(SomeMethod)); + } - private static async Task SomeMethod(int i) + private static async Task SomeMethod(int i) + { + var asyncLock = new AsyncLock(); + System.Diagnostics.Debug.WriteLine($"Outside {i}"); + await Task.Delay(100); + using (await asyncLock.LockAsync()) { - var asyncLock = new AsyncLock(); - System.Diagnostics.Debug.WriteLine($"Outside {i}"); + System.Diagnostics.Debug.WriteLine($"Lock1 {i}"); await Task.Delay(100); using (await asyncLock.LockAsync()) { - System.Diagnostics.Debug.WriteLine($"Lock1 {i}"); + System.Diagnostics.Debug.WriteLine($"Lock2 {i}"); await Task.Delay(100); - using (await asyncLock.LockAsync()) - { - System.Diagnostics.Debug.WriteLine($"Lock2 {i}"); - await Task.Delay(100); - } } } } diff --git a/UnitTests/ReentracePermittedTests.cs b/UnitTests/ReentracePermittedTests.cs index a4d8e3a..45698ad 100644 --- a/UnitTests/ReentracePermittedTests.cs +++ b/UnitTests/ReentracePermittedTests.cs @@ -3,73 +3,72 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NeoSmart.AsyncLock; -namespace AsyncLockTests +namespace AsyncLockTests; + +[TestClass] +public class ReentracePermittedTests { - [TestClass] - public class ReentracePermittedTests + private readonly AsyncLock _lock = new(); + + [TestMethod] + public async Task NestedCallReentrance() { - readonly AsyncLock _lock = new AsyncLock(); + using (await _lock.LockAsync()) + using (await _lock.LockAsync()) + { + Debug.WriteLine("Hello from NestedCallReentrance!"); + } + } - [TestMethod] - public async Task NestedCallReentrance() + [TestMethod] + public void NestedAsyncCallReentrance() + { + var task = Task.Run(async () => { 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(); - } + new TaskWaiter(task).WaitOne(); + } - private async Task NestedFunctionAsync() + private async Task NestedFunctionAsync() + { + using (await _lock.LockAsync()) { - using (await _lock.LockAsync()) - { - Debug.WriteLine("Hello from another (nested) function!"); - } + Debug.WriteLine("Hello from another (nested) function!"); } + } - [TestMethod] - public async Task NestedFunctionCallReentrance() + [TestMethod] + public async Task NestedFunctionCallReentrance() + { + using (await _lock.LockAsync()) { - using (await _lock.LockAsync()) - { - await NestedFunctionAsync(); - } + await NestedFunctionAsync(); } + } - // Issue #18 - [TestMethod] - //[Timeout(5)] - public async Task BackToBackReentrance() + // Issue #18 + [TestMethod] + //[Timeout(5)] + public async Task BackToBackReentrance() + { + var asyncLock = new AsyncLock(); + async Task InnerFunctionAsync() { - var asyncLock = new AsyncLock(); - async Task InnerFunctionAsync() - { - using (await asyncLock.LockAsync()) - { - // - } - } using (await asyncLock.LockAsync()) { - await InnerFunctionAsync(); - await InnerFunctionAsync(); + // } } + using (await asyncLock.LockAsync()) + { + await InnerFunctionAsync(); + await InnerFunctionAsync(); + } } } diff --git a/UnitTests/ReentranceLockoutTests.cs b/UnitTests/ReentranceLockoutTests.cs index e7224be..fd46fcd 100644 --- a/UnitTests/ReentranceLockoutTests.cs +++ b/UnitTests/ReentranceLockoutTests.cs @@ -1,99 +1,78 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using NeoSmart.AsyncLock; -namespace AsyncLockTests +namespace AsyncLockTests; + +[TestClass] +public class ReentranceLockoutTests { - [TestClass] - public class ReentranceLockoutTests - { - private AsyncLock _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 AsyncLock _lock; + private LimitedResource _resource; + private CountdownEvent _countdown; + private readonly Random _random = new((int)DateTime.UtcNow.Ticks); + private int DelayInterval => _random.Next(1, 5) * 10; - private void ResourceSimulation(Action action) + private void ResourceSimulation(Action action) + { + _lock = new AsyncLock(); + // Start n threads and have them obtain the lock and randomly wait, then verify + var failure = new ManualResetEventSlim(false); + _resource = new LimitedResource(() => { - _lock = new AsyncLock(); - // Start n threads and have them obtain the lock and randomly wait, then verify - var failure = new ManualResetEventSlim(false); - _resource = new LimitedResource(() => - { - failure.Set(); - }); + failure.Set(); + }); - var testCount = 20; - _countdown = new CountdownEvent(testCount); + 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!"); - } + for (int i = 0; i < testCount; ++i) + { + action(); } - private async void ThreadEntryPoint() + if (WaitHandle.WaitAny([_countdown.WaitHandle, failure.WaitHandle]) == 1) { - using (await _lock.LockAsync()) - { - _resource.BeginSomethingDangerous(); - Thread.Sleep(DelayInterval); - _resource.EndSomethingDangerous(); - } - _countdown.Signal(); + Assert.Fail("More than one thread simultaneously accessed the underlying resource!"); } + } - /// - /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing a function entrypoint. - /// - [TestMethod] - public void MultipleThreadsMethodLockout() + private async void ThreadEntryPoint() + { + using (await _lock.LockAsync()) { - ResourceSimulation(() => - { - var t = new Thread(ThreadEntryPoint); - t.Start(); - }); + _resource.BeginSomethingDangerous(); + Thread.Sleep(DelayInterval); + _resource.EndSomethingDangerous(); } + _countdown.Signal(); + } - /// - /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing nothing. - /// - [TestMethod] - public void MultipleThreadsLockout() + /// + /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing a function entrypoint. + /// + [TestMethod] + public void MultipleThreadsMethodLockout() + { + ResourceSimulation(() => { - ResourceSimulation(() => - { - var t = new Thread(async () => - { - using (await _lock.LockAsync()) - { - _resource.BeginSomethingDangerous(); - Thread.Sleep(DelayInterval); - _resource.EndSomethingDangerous(); - } - _countdown.Signal(); - }); - t.Start(); - }); - } + var t = new Thread(ThreadEntryPoint); + t.Start(); + }); + } - /// - /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing a local ThreadStart - /// - [TestMethod] - public void MultipleThreadsThreadStartLockout() + /// + /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing nothing. + /// + [TestMethod] + public void MultipleThreadsLockout() + { + ResourceSimulation(() => { - ThreadStart work = async () => + var t = new Thread(async () => { using (await _lock.LockAsync()) { @@ -102,73 +81,92 @@ public void MultipleThreadsThreadStartLockout() _resource.EndSomethingDangerous(); } _countdown.Signal(); - }; + }); + t.Start(); + }); + } - ResourceSimulation(() => + /// + /// 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()) { - var t = new Thread(work); - t.Start(); - }); - } + _resource.BeginSomethingDangerous(); + Thread.Sleep(DelayInterval); + _resource.EndSomethingDangerous(); + } + _countdown.Signal(); + }; - [TestMethod] - public void AsyncLockout() + ResourceSimulation(() => { - ResourceSimulation(() => + var t = new Thread(work); + t.Start(); + }); + } + + [TestMethod] + public void AsyncLockout() + { + ResourceSimulation(() => + { + Task.Run(async () => { - Task.Run(async () => + using (await _lock.LockAsync()) { - using (await _lock.LockAsync()) - { - _resource.BeginSomethingDangerous(); - Thread.Sleep(DelayInterval); - _resource.EndSomethingDangerous(); - } - _countdown.Signal(); - }); + _resource.BeginSomethingDangerous(); + Thread.Sleep(DelayInterval); + _resource.EndSomethingDangerous(); + } + _countdown.Signal(); }); - } + }); + } - [TestMethod] - public void AsyncDelayLockout() + [TestMethod] + public void AsyncDelayLockout() + { + ResourceSimulation(() => { - ResourceSimulation(() => + Task.Run(async () => { - Task.Run(async () => + using (await _lock.LockAsync()) { - using (await _lock.LockAsync()) - { - _resource.BeginSomethingDangerous(); - await Task.Delay(DelayInterval); - _resource.EndSomethingDangerous(); - } - _countdown.Signal(); - }); + _resource.BeginSomethingDangerous(); + await Task.Delay(DelayInterval); + _resource.EndSomethingDangerous(); + } + _countdown.Signal(); }); - } + }); + } - [TestMethod] - public async Task NestedAsyncLockout() + [TestMethod] + public async Task NestedAsyncLockout() + { + var taskStarted = new SemaphoreSlim(0, 1); + var taskEnded = new SemaphoreSlim(0, 1); + var @lock = new AsyncLock(); + using (await @lock.LockAsync()) { - var taskStarted = new SemaphoreSlim(0, 1); - var taskEnded = new SemaphoreSlim(0, 1); - var @lock = new AsyncLock(); - using (await @lock.LockAsync()) + var task = Task.Run(async () => { - var task = Task.Run(async () => + taskStarted.Release(); + using (await @lock.LockAsync()) { - taskStarted.Release(); - using (await @lock.LockAsync()) - { - Debug.WriteLine("Hello from within an async task!"); - } - await taskEnded.WaitAsync(); - }); + Debug.WriteLine("Hello from within an async task!"); + } + await taskEnded.WaitAsync(); + }); - taskStarted.Wait(); - Assert.IsFalse(new TaskWaiter(task).WaitOne(100)); - taskEnded.Release(); - } + taskStarted.Wait(); + Assert.IsFalse(new TaskWaiter(task).WaitOne(100)); + taskEnded.Release(); } } } diff --git a/UnitTests/TaskWaiter.cs b/UnitTests/TaskWaiter.cs index 1278830..68155f6 100644 --- a/UnitTests/TaskWaiter.cs +++ b/UnitTests/TaskWaiter.cs @@ -1,25 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; +using System.Threading; using System.Threading.Tasks; -namespace AsyncLockTests +namespace AsyncLockTests; + +/// +/// A guaranteed-safe method of "synchronously" waiting on tasks to finish. Cannot be used on .NET Core +/// +class TaskWaiter : EventWaitHandle { - /// - /// A guaranteed-safe method of "synchronously" waiting on tasks to finish. Cannot be used on .NET Core - /// - class TaskWaiter : EventWaitHandle + public TaskWaiter(Task task) + : base(false, EventResetMode.ManualReset) { - public TaskWaiter(Task task) - : base(false, EventResetMode.ManualReset) + new Thread(async () => { - new Thread(async () => - { - await task; - Set(); - }).Start(); - } + await task; + Set(); + }).Start(); } } diff --git a/UnitTests/TryLockTests.cs b/UnitTests/TryLockTests.cs index 958a033..9d0c0a7 100644 --- a/UnitTests/TryLockTests.cs +++ b/UnitTests/TryLockTests.cs @@ -3,81 +3,80 @@ using System; using System.Threading; -namespace AsyncLockTests +namespace AsyncLockTests; + +[TestClass] +public class TryLockTests { - [TestClass] - public class TryLockTests + [TestMethod] + public void NoContention() { - [TestMethod] - public void NoContention() - { - var @lock = new AsyncLock(); + var @lock = new AsyncLock(); - Assert.IsTrue(@lock.TryLock(() => { }, default)); - } + Assert.IsTrue(@lock.TryLock(() => { }, default)); + } - [TestMethod] - public void ContentionEarlyReturn() - { - var @lock = new AsyncLock(); + [TestMethod] + public void ContentionEarlyReturn() + { + var @lock = new AsyncLock(); - using (@lock.Lock()) + using (@lock.Lock()) + { + var thread = new Thread(() => { - var thread = new Thread(() => - { - Assert.IsFalse(@lock.TryLock(() => throw new Exception("This should never be executed"), default)); - }); - thread.Start(); - thread.Join(); - } + 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 ContentionDelayedExecution() => ContentionalExecution(50, 250, true); - [TestMethod] - public void ContentionNoExecution() => ContentionalExecution(250, 50, false); + [TestMethod] + public void ContentionNoExecution() => ContentionalExecution(250, 50, false); - [TestMethod] - public void ContentionNoExecutionZeroTimeout() => ContentionalExecution(250, 0, false); + [TestMethod] + public void ContentionNoExecutionZeroTimeout() => ContentionalExecution(250, 0, false); - private void ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult) - { - int step = 0; - var @lock = new AsyncLock(); + private void ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult) + { + int step = 0; + var @lock = new AsyncLock(); - var locked = @lock.Lock(); - Interlocked.Increment(ref step); + 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); + 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 unlockThread = new Thread(() => + { + eventTestThreadStarted.WaitOne(); + eventSleepNotStarted.Set(); + Thread.Sleep(unlockDelayMs); + eventAboutToWait.WaitOne(); + Interlocked.Increment(ref step); + locked.Dispose(); + }); + unlockThread.Start(); - var testThread = new Thread(() => + var testThread = new Thread(() => + { + eventTestThreadStarted.Set(); + eventSleepNotStarted.WaitOne(); + eventAboutToWait.Set(); + Assert.IsTrue((!expectedResult) ^ @lock.TryLock(() => { - eventTestThreadStarted.Set(); - eventSleepNotStarted.WaitOne(); - eventAboutToWait.Set(); - Assert.IsTrue((!expectedResult) ^ @lock.TryLock(() => - { - Assert.AreEqual(2, step); - }, TimeSpan.FromMilliseconds(lockTimeoutMs))); - }); - testThread.Start(); + Assert.AreEqual(2, step); + }, TimeSpan.FromMilliseconds(lockTimeoutMs))); + }); + testThread.Start(); - unlockThread.Join(); - testThread.Join(); - } + unlockThread.Join(); + testThread.Join(); } } diff --git a/UnitTests/TryLockTestsAsync.cs b/UnitTests/TryLockTestsAsync.cs index e33addf..2274dfe 100644 --- a/UnitTests/TryLockTestsAsync.cs +++ b/UnitTests/TryLockTestsAsync.cs @@ -4,103 +4,102 @@ using System.Threading; using System.Threading.Tasks; -namespace AsyncLockTests +namespace AsyncLockTests; + +class LocalException : Exception { + public LocalException(string message) : base(message) { } +} + +[TestClass] +public class TryLockTestsAsync { - class LocalException : Exception { - public LocalException(string message) : base(message) { } + [TestMethod] + public async Task NoContention() + { + var @lock = new AsyncLock(); + + Assert.IsTrue(await @lock.TryLockAsync(() => { }, TimeSpan.Zero)); } - [TestClass] - public class TryLockTestsAsync + /// + /// Assert that exceptions are bubbled up after the lock is disposed + /// + /// + [TestMethod] + public async Task NoContentionThrows() { - [TestMethod] - public async Task NoContention() + var @lock = new AsyncLock(); + + await Assert.ThrowsExactlyAsync(async () => { - var @lock = new AsyncLock(); + await @lock.TryLockAsync(async () => { + await Task.Yield(); + throw new LocalException("This exception needs to be bubbled up"); + }, TimeSpan.Zero); + }); + } - Assert.IsTrue(await @lock.TryLockAsync(() => { }, TimeSpan.Zero)); - } + [TestMethod] + public async Task ContentionEarlyReturn() + { + var @lock = new AsyncLock(); - /// - /// Assert that exceptions are bubbled up after the lock is disposed - /// - /// - [TestMethod] - public async Task NoContentionThrows() + using (await @lock.LockAsync()) { - var @lock = new AsyncLock(); - - await Assert.ThrowsExceptionAsync(async () => + var thread = new Thread(async () => { - await @lock.TryLockAsync(async () => { - await Task.Yield(); - throw new LocalException("This exception needs to be bubbled up"); - }, TimeSpan.Zero); + Assert.IsFalse(await @lock.TryLockAsync(() => throw new Exception("This should never be executed"), TimeSpan.Zero)); }); + thread.Start(); + thread.Join(); } + } - [TestMethod] - public async Task ContentionEarlyReturn() - { - var @lock = new AsyncLock(); + [TestMethod] + public async Task ContentionDelayedExecution() => await ContentionalExecution(50, 250, true); - using (await @lock.LockAsync()) - { - var thread = new Thread(async () => - { - Assert.IsFalse(await @lock.TryLockAsync(() => throw new Exception("This should never be executed"), TimeSpan.Zero)); - }); - thread.Start(); - thread.Join(); - } - } + [TestMethod] + public async Task ContentionNoExecution() => await ContentionalExecution(250, 50, false); + + [TestMethod] + public async Task ContentionNoExecutionZeroTimeout() => await ContentionalExecution(250, 0, false); - [TestMethod] - public async Task ContentionDelayedExecution() => await ContentionalExecution(50, 250, true); + private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult) + { + int step = 0; + var @lock = new AsyncLock(); - [TestMethod] - public async Task ContentionNoExecution() => await ContentionalExecution(250, 50, false); + var locked = await @lock.LockAsync(); + Interlocked.Increment(ref step); - [TestMethod] - public async Task ContentionNoExecutionZeroTimeout() => await ContentionalExecution(250, 0, false); + using var eventTestThreadStarted = new SemaphoreSlim(0, 1); + using var eventSleepNotStarted = new SemaphoreSlim(0, 1); + using var eventAboutToWait = new SemaphoreSlim(0, 1); - private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult) + var unlockThread = new Thread(async () => { - int step = 0; - var @lock = new AsyncLock(); - - var locked = await @lock.LockAsync(); + await eventTestThreadStarted.WaitAsync(); + eventSleepNotStarted.Release(); + Thread.Sleep(unlockDelayMs); + await eventAboutToWait.WaitAsync(); Interlocked.Increment(ref step); + locked.Dispose(); + }); + unlockThread.Start(); - 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 () => + var testThread = new Thread(async () => + { + eventTestThreadStarted.Release(); + await eventSleepNotStarted.WaitAsync(); + eventAboutToWait.Release(); + Assert.IsTrue((!expectedResult) ^ await @lock.TryLockAsync(() => { - eventTestThreadStarted.Release(); - await eventSleepNotStarted.WaitAsync(); - eventAboutToWait.Release(); - Assert.IsTrue((!expectedResult) ^ await @lock.TryLockAsync(() => - { - Assert.AreEqual(2, step); - }, TimeSpan.FromMilliseconds(lockTimeoutMs))); - }); - testThread.Start(); + Assert.AreEqual(2, step); + }, TimeSpan.FromMilliseconds(lockTimeoutMs))); + }); + testThread.Start(); - unlockThread.Join(); - testThread.Join(); - } + unlockThread.Join(); + testThread.Join(); } } diff --git a/UnitTests/TryLockTestsAsyncOut.cs b/UnitTests/TryLockTestsAsyncOut.cs deleted file mode 100644 index 1d59ab0..0000000 --- a/UnitTests/TryLockTestsAsyncOut.cs +++ /dev/null @@ -1,117 +0,0 @@ -#if TRY_LOCK_OUT_BOOL - -using Microsoft.VisualStudio.TestTools.UnitTesting; -using NeoSmart.AsyncLock; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace AsyncLockTests -{ - [TestClass] - public class TryLockTestsAsyncOut - { - [TestMethod] - public async Task NoContention() - { - var @lock = new AsyncLock(); - - 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 AsyncLock(); - - 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() - { - var @lock = new AsyncLock(); - - using (await @lock.LockAsync()) - { - var thread = new Thread(async () => - { - await Task.Yield(); - var disposable = @lock.TryLockAsync(TimeSpan.Zero, out var locked); - Assert.IsFalse(locked); - }); - thread.Start(); - thread.Join(); - } - } - - [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; - var @lock = new AsyncLock(); - - 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(); - - await @lock.TryLockAsync(TimeSpan.FromMilliseconds(lockTimeoutMs), out var locked); - Assert.IsTrue((!expectedResult) ^ locked); - - if (locked) - { - Assert.AreEqual(2, step); - - } - }); - testThread.Start(); - - unlockThread.Join(); - testThread.Join(); - } - } -} - -#endif diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 45508e9..39b4414 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -1,7 +1,7 @@ - net5.0 + net9.0 false @@ -9,10 +9,10 @@ - - - - + + + +