diff --git a/src/THNETII.Common/Threading/SemaphoreSlimContext.cs b/src/THNETII.Common/Threading/SemaphoreSlimContext.cs new file mode 100644 index 00000000..27a87b63 --- /dev/null +++ b/src/THNETII.Common/Threading/SemaphoreSlimContext.cs @@ -0,0 +1,459 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace THNETII.Common.Threading +{ + /// + /// A disposable context token that can be used to track the lifetime during + /// which an application holds access to a + /// instance. + /// + /// When the context is disposed (or garbage collected), the + /// method on the original + /// instance is invoked. + /// + /// + /// + /// Using this class allows developers to wrap a critical section protected + /// by a semaphore in a using code-block. This guarantees for safe + /// resource management similar to the lock code-block. However, in + /// addition to the lock block, the + /// class also supports asynchronous locking through its Async-methods. + /// + public class SemaphoreSlimContext : IDisposable + { + private readonly SemaphoreSlim semaphore; + + /// + /// Gets the number of times the object + /// that is captured by the current instance should be released. + /// + /// An value greater than 0 (zero). + /// + /// The returned value corresponds to the value that is passed as the + /// argument value to when + /// is invoked. + /// + protected int ReleaseCount { get; } + + private SemaphoreSlimContext(SemaphoreSlim semaphore, int releaseCount = 1, bool verify = false) + { + Debug.Assert(!verify, nameof(verify) + " is not false."); + this.semaphore = semaphore; + ReleaseCount = releaseCount; + } + + /// + /// Creates a new semaphore context for the specified and already + /// acquired instance. + /// + /// The instance that has been acquired by the application. + /// The number of times the semaphore has been acquired. Defaults to 1. + /// is null. + /// is less than 1. + protected SemaphoreSlimContext(SemaphoreSlim semaphore, + int releaseCount = 1) + : this( + semaphore ?? throw new ArgumentNullException(nameof(semaphore)), + releaseCount >= 1 ? releaseCount : throw new ArgumentOutOfRangeException(nameof(releaseCount), releaseCount, nameof(releaseCount) + " is less than 1"), + verify: false + ) + { } + + internal static SemaphoreSlimContext CreateNoVerify(SemaphoreSlim semaphore, + int releaseCount = 1) => + new SemaphoreSlimContext(semaphore, releaseCount, verify: false); + + /// + /// Blocks the current thread until it can enter the + /// and returns a context object that relases the semaphore when it is + /// disposed. + /// + /// The semaphore to enter. + /// + /// A instance that captures + /// and releases the semaphore once its + /// method is invoked. + /// + /// is null. + /// instance has been disposed. + public static SemaphoreSlimContext CreateOnEnter(SemaphoreSlim semaphore) + { + semaphore.ThrowIfNull(nameof(semaphore)).Wait(); + return new SemaphoreSlimContext(semaphore, verify: false); + } + + /// + /// Blocks the current thread until it can enter the , + /// while observing a + /// and returns a context object that relases the semaphore when it is + /// disposed. + /// + /// The semaphore to enter. + /// The token to observe. + /// + /// A instance that captures + /// and releases the semaphore once its + /// method is invoked. + /// + /// is null. + /// was canceled. + /// + /// instance has been disposed.
+ /// -or-
+ /// The that created has already been disposed. + ///
+ public static SemaphoreSlimContext CreateOnEnter(SemaphoreSlim semaphore, + CancellationToken cancelToken) + { + semaphore.ThrowIfNull(nameof(semaphore)).Wait(cancelToken); + return new SemaphoreSlimContext(semaphore, verify: false); + } + + /// + /// Blocks the current thread until it can enter the , + /// using a 32-bit integer that specifies the timeout + /// and returns a context object that relases the semaphore when it is + /// disposed. + /// + /// The semaphore to enter. + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + /// + /// null if the timeout expired before the semphore could be entered; otherwise, + /// a instance that captures + /// and releases the semaphore once its + /// method is invoked. + /// + /// is null. + /// is a negative value other than -1, which represents an infinite time-out. + /// instance has been disposed. + public static SemaphoreSlimContext CreateOnEnter(SemaphoreSlim semaphore, + int millisecondsTimeout) + { + bool entered = semaphore.ThrowIfNull(nameof(semaphore)) + .Wait(millisecondsTimeout); + return entered + ? new SemaphoreSlimContext(semaphore, verify: false) + : null; + } + + /// + /// Blocks the current thread until it can enter the , + /// using a 32-bit integer that specifies the timeout, while observing a + /// and returns a context object that relases the semaphore when it is + /// disposed. + /// + /// The semaphore to enter. + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + /// The token to observe. + /// + /// null if the timeout expired before the semphore could be entered; otherwise, + /// a instance that captures + /// and releases the semaphore once its + /// method is invoked. + /// + /// is null. + /// is a negative value other than -1, which represents an infinite time-out. + /// was canceled. + /// + /// instance has been disposed.
+ /// -or-
+ /// The that created has already been disposed. + ///
+ public static SemaphoreSlimContext CreateOnEnter(SemaphoreSlim semaphore, + int millisecondsTimeout, CancellationToken cancelToken) + { + bool entered = semaphore.ThrowIfNull(nameof(semaphore)) + .Wait(millisecondsTimeout, cancelToken); + return entered + ? new SemaphoreSlimContext(semaphore, verify: false) + : null; + } + + /// + /// Blocks the current thread until it can enter the , + /// using the specified timeout + /// and returns a context object that relases the semaphore when it is + /// disposed. + /// + /// The semaphore to enter. + /// A value that represents time to wait, or a that represents -1 milliseconds to wait indefinitely. + /// + /// null if the timeout expired before the semphore could be entered; otherwise, + /// a instance that captures + /// and releases the semaphore once its + /// method is invoked. + /// + /// is null. + /// is a negative value other than -1 milliseconds, which represents an infinite time-out. + /// instance has been disposed. + public static SemaphoreSlimContext CreateOnEnter(SemaphoreSlim semaphore, + TimeSpan timeout) + { + bool entered = semaphore.ThrowIfNull(nameof(semaphore)) + .Wait(timeout); + return entered + ? new SemaphoreSlimContext(semaphore, verify: false) + : null; + } + + /// + /// Blocks the current thread until it can enter the , + /// using the specified timeout while observing a + /// and returns a context object that relases the semaphore when it is + /// disposed. + /// + /// The semaphore to enter. + /// A value that represents time to wait, or a that represents -1 milliseconds to wait indefinitely. + /// The token to observe. + /// + /// null if the timeout expired before the semphore could be entered; otherwise, + /// a instance that captures + /// and releases the semaphore once its + /// method is invoked. + /// + /// is null. + /// is a negative value other than -1 milliseconds, which represents an infinite time-out. + /// was canceled. + /// + /// instance has been disposed.
+ /// -or-
+ /// The that created has already been disposed. + ///
+ public static SemaphoreSlimContext CreateOnEnter(SemaphoreSlim semaphore, + TimeSpan timeout, CancellationToken cancelToken) + { + bool entered = semaphore.ThrowIfNull(nameof(semaphore)) + .Wait(timeout, cancelToken); + return entered + ? new SemaphoreSlimContext(semaphore, verify: false) + : null; + } + + /// + /// Asynchronously waits to enter the and + /// returns a context object that releases the semaphore when it is + /// disposed. + /// + /// The semaphore to enter. + /// A task that will complete when the semaphore has been entered. + /// is null. + /// has already been disposed. + public static async Task CreateOnEnterAsync( + SemaphoreSlim semaphore) + { + await semaphore.ThrowIfNull(nameof(semaphore)).WaitAsync() + .ConfigureAwait(continueOnCapturedContext: false); + return new SemaphoreSlimContext(semaphore, verify: false); + } + + /// + /// Asynchronously waits to enter a , + /// while observing a and + /// returns a context object that releases the semaphore when it is + /// disposed. + /// + /// A task that will complete when the semaphore has been entered. + /// The semaphore to enter. + /// The token to observe. + /// is null. + /// + /// instance has been disposed.
+ /// -or-
+ /// The that created has already been disposed. + ///
+ /// was canceled. + public static async Task CreateOnEnterAsync( + SemaphoreSlim semaphore, CancellationToken cancelToken) + { + await semaphore.ThrowIfNull(nameof(semaphore)).WaitAsync(cancelToken) + .ConfigureAwait(continueOnCapturedContext: false); + return new SemaphoreSlimContext(semaphore, verify: false); + } + + /// + /// Asynchronously waits to enter a , + /// using a 32-bit signed integer to measure the time interval and + /// returns a context object that releases the semaphore when it is + /// disposed. + /// + /// + /// A task that will complete with a result of a context object that + /// releases the semaphore when disposed if the current thread + /// successfully entered the , + /// otherwise with a result of null. + /// The semaphore to enter. + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + /// is null. + /// is a negative number other than -1, which represents an infinite time-out. + /// has already been disposed. + public static async Task CreateOnEnterAsync( + SemaphoreSlim semaphore, int millisecondsTimeout) + { + bool entered = await semaphore.ThrowIfNull(nameof(semaphore)) + .WaitAsync(millisecondsTimeout) + .ConfigureAwait(continueOnCapturedContext: false); + return entered + ? new SemaphoreSlimContext(semaphore, verify: false) + : null; + } + + /// + /// Asynchronously waits to enter a , + /// using a 32-bit signed integer to measure the time interval + /// while observing a and + /// returns a context object that releases the semaphore when it is + /// disposed. + /// + /// + /// A task that will complete with a result of a context object that + /// releases the semaphore when disposed if the current thread + /// successfully entered the , + /// otherwise with a result of null. + /// The semaphore to enter. + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + /// The token to observe. + /// is null. + /// is a negative number other than -1, which represents an infinite time-out. + /// + /// instance has been disposed.
+ /// -or-
+ /// The that created has already been disposed. + ///
+ /// was canceled. + public static async Task CreateOnEnterAsync( + SemaphoreSlim semaphore, int millisecondsTimeout, + CancellationToken cancelToken) + { + bool entered = await semaphore.ThrowIfNull(nameof(semaphore)) + .WaitAsync(millisecondsTimeout, cancelToken) + .ConfigureAwait(continueOnCapturedContext: false); + return entered + ? new SemaphoreSlimContext(semaphore, verify: false) + : null; + } + + /// + /// Asynchronously waits to enter a , + /// using the specified timeout and + /// returns a context object that releases the semaphore when it is + /// disposed. + /// + /// + /// A task that will complete with a result of a context object that + /// releases the semaphore when disposed if the current thread + /// successfully entered the , + /// otherwise with a result of null. + /// The semaphore to enter. + /// The time to wait, or a value representing -1 milliseconds to wait indefinitely. + /// is null. + /// is a negative value other than -1 milliseconds, which represents an infinite time-out. + /// has already been disposed. + public static async Task CreateOnEnterAsync( + SemaphoreSlim semaphore, TimeSpan timeout) + { + bool entered = await semaphore.ThrowIfNull(nameof(semaphore)) + .WaitAsync(timeout) + .ConfigureAwait(continueOnCapturedContext: false); + return entered + ? new SemaphoreSlimContext(semaphore, verify: false) + : null; + } + + /// + /// Asynchronously waits to enter a , + /// using the specified timeout and + /// while observing a and + /// returns a context object that releases the semaphore when it is + /// disposed. + /// + /// + /// A task that will complete with a result of a context object that + /// releases the semaphore when disposed if the current thread + /// successfully entered the , + /// otherwise with a result of null. + /// The semaphore to enter. + /// The time to wait, or a value representing -1 milliseconds to wait indefinitely. + /// The token to observe. + /// is null. + /// is a negative value other than -1 milliseconds, which represents an infinite time-out. + /// + /// instance has been disposed.
+ /// -or-
+ /// The that created has already been disposed. + ///
+ /// was canceled. + public static async Task CreateOnEnterAsync( + SemaphoreSlim semaphore, TimeSpan timeout, + CancellationToken cancelToken) + { + bool entered = await semaphore.ThrowIfNull(nameof(semaphore)) + .WaitAsync(timeout, cancelToken) + .ConfigureAwait(continueOnCapturedContext: false); + return entered + ? new SemaphoreSlimContext(semaphore, verify: false) + : null; + } + + #region IDisposable Support + private int disposedValue = 0; // To detect redundant calls + + /// + /// Releases the captured semaphore by calling + /// with + /// as the argument value. + /// + /// This method is thread-safe and guaranteed to only release the + /// semaphore exactly once during the lifetime of this context instance. + /// + /// + /// + /// true if called from within an implementation of + /// , false if called from + /// within the finalizer. + /// + /// + /// The method atomically flips a state flag variable when the method is + /// executed the first time. Thus, subsequent calls to + /// have no effect. + /// + protected virtual void Dispose(bool disposing) + { + if (Interlocked.Exchange(ref disposedValue, 1) == 0) + { + semaphore?.Release(ReleaseCount); + } + } + + /// + /// Releases the semaphore instance. + /// + /// + /// This method is thread-safe. The semaphore is only released once. + /// After the initial call to , subsequent calls + /// have no effect. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the semaphore object captured by this instance when this is + /// instance is garbage collected. + /// + /// + /// Applications should not rely on garbage collection to properly + /// release held semaphore objects. Instead the current context instance + /// should be used in a using code-block. + /// + ~SemaphoreSlimContext() + { + Dispose(false); + } + #endregion + } +} diff --git a/src/THNETII.Common/Threading/SemaphoreSlimExtensions.cs b/src/THNETII.Common/Threading/SemaphoreSlimExtensions.cs new file mode 100644 index 00000000..6ba75df0 --- /dev/null +++ b/src/THNETII.Common/Threading/SemaphoreSlimExtensions.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace THNETII.Common.Threading +{ + /// + /// Provides extension methods for the class. + /// + public static class SemaphoreSlimExtensions + { + /// + public static SemaphoreSlimContext WaitContext(this SemaphoreSlim semaphore) => + SemaphoreSlimContext.CreateOnEnter(semaphore); + + /// + public static SemaphoreSlimContext WaitContext(this SemaphoreSlim semaphore, + int millisecondsTimeout) => + SemaphoreSlimContext.CreateOnEnter(semaphore, millisecondsTimeout); + + /// + public static SemaphoreSlimContext WaitContext(this SemaphoreSlim semaphore, + TimeSpan timeout) => + SemaphoreSlimContext.CreateOnEnter(semaphore, timeout); + + /// + public static SemaphoreSlimContext WaitContext(this SemaphoreSlim semaphore, + CancellationToken cancelToken) => + SemaphoreSlimContext.CreateOnEnter(semaphore, cancelToken); + + /// + public static SemaphoreSlimContext WaitContext(this SemaphoreSlim semaphore, + int millisecondsTimeout, CancellationToken cancelToken) => + SemaphoreSlimContext.CreateOnEnter(semaphore, millisecondsTimeout, cancelToken); + + /// + public static SemaphoreSlimContext WaitContext(this SemaphoreSlim semaphore, + TimeSpan timeout, CancellationToken cancelToken) => + SemaphoreSlimContext.CreateOnEnter(semaphore, timeout, cancelToken); + + /// + public static Task WaitContextAsync( + this SemaphoreSlim semaphore) => + SemaphoreSlimContext.CreateOnEnterAsync(semaphore); + + /// + public static Task WaitContextAsync( + this SemaphoreSlim semaphore, int millisecondsTimeout) => + SemaphoreSlimContext.CreateOnEnterAsync(semaphore, millisecondsTimeout); + + /// + public static Task WaitContextAsync( + this SemaphoreSlim semaphore, TimeSpan timeout) => + SemaphoreSlimContext.CreateOnEnterAsync(semaphore, timeout); + + /// + public static Task WaitContextAsync( + this SemaphoreSlim semaphore, CancellationToken cancelToken) => + SemaphoreSlimContext.CreateOnEnterAsync(semaphore, cancelToken); + + /// + public static Task WaitContextAsync( + this SemaphoreSlim semaphore, int millisecondsTimeout, + CancellationToken cancelToken) => + SemaphoreSlimContext.CreateOnEnterAsync(semaphore, millisecondsTimeout, cancelToken); + + /// + public static Task WaitContextAsync( + this SemaphoreSlim semaphore, TimeSpan timeout, + CancellationToken cancelToken) => + SemaphoreSlimContext.CreateOnEnterAsync(semaphore, timeout, cancelToken); + } +}