@ -6,20 +6,58 @@ using System.Threading.Tasks;
namespace Volo.Abp.Threading ;
namespace Volo.Abp.Threading ;
/// <summary>
/// <summary>
/// Per-key asynchronous lock.
/// Per-key asynchronous lock for coordinating concurrent flows.
/// https://stackoverflow.com/a/31194647
/// </summary>
/// </summary>
/// <remarks>
/// Based on the pattern described in https://stackoverflow.com/a/31194647.
/// Use within a <c>using</c> scope to ensure the lock is released via <c>IDisposable.Dispose()</c>.
/// </remarks>
public static class KeyedLock
public static class KeyedLock
{
{
private static readonly Dictionary < object , RefCounted < SemaphoreSlim > > SemaphoreSlims = new ( ) ;
private static readonly Dictionary < object , RefCounted < SemaphoreSlim > > SemaphoreSlims = new ( ) ;
/// <summary>
/// Acquires an exclusive asynchronous lock for the specified <paramref name="key"/>.
/// This method waits until the lock becomes available.
/// </summary>
/// <param name="key">A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock.</param>
/// <returns>An <see cref="IDisposable"/> handle that must be disposed to release the lock.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
/// <example>
/// <code>
/// var key = "my-critical-section";
/// using (await KeyedLock.LockAsync(key))
/// {
/// // protected work
/// }
/// </code>
/// </example>
public static async Task < IDisposable > LockAsync ( object key )
public static async Task < IDisposable > LockAsync ( object key )
{
{
Check . NotNull ( key , nameof ( key ) ) ;
return await LockAsync ( key , CancellationToken . None ) ;
return await LockAsync ( key , CancellationToken . None ) ;
}
}
/// <summary>
/// Acquires an exclusive asynchronous lock for the specified <paramref name="key"/>, observing a <paramref name="cancellationToken"/>.
/// </summary>
/// <param name="key">A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock.</param>
/// <param name="cancellationToken">A token to cancel the wait for the lock.</param>
/// <returns>An <see cref="IDisposable"/> handle that must be disposed to release the lock.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
/// <exception cref="OperationCanceledException">Thrown if the wait is canceled via <paramref name="cancellationToken"/>.</exception>
/// <example>
/// <code>
/// var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
/// using (await KeyedLock.LockAsync("db-update", cts.Token))
/// {
/// // protected work
/// }
/// </code>
/// </example>
public static async Task < IDisposable > LockAsync ( object key , CancellationToken cancellationToken )
public static async Task < IDisposable > LockAsync ( object key , CancellationToken cancellationToken )
{
{
Check . NotNull ( key , nameof ( key ) ) ;
var semaphore = GetOrCreate ( key ) ;
var semaphore = GetOrCreate ( key ) ;
try
try
{
{
@ -34,13 +72,62 @@ public static class KeyedLock
return new Releaser ( key ) ;
return new Releaser ( key ) ;
}
}
/// <summary>
/// Attempts to acquire an exclusive lock for the specified <paramref name="key"/> without waiting.
/// </summary>
/// <param name="key">A non-null object that identifies the lock.</param>
/// <returns>
/// An <see cref="IDisposable"/> handle if the lock was immediately acquired; otherwise <see langword="null"/>.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
/// <example>
/// <code>
/// var handle = await KeyedLock.TryLockAsync("cache-key");
/// if (handle != null)
/// {
/// using (handle)
/// {
/// // protected work
/// }
/// }
/// </code>
/// </example>
public static async Task < IDisposable ? > TryLockAsync ( object key )
public static async Task < IDisposable ? > TryLockAsync ( object key )
{
{
Check . NotNull ( key , nameof ( key ) ) ;
return await TryLockAsync ( key , default , CancellationToken . None ) ;
return await TryLockAsync ( key , default , CancellationToken . None ) ;
}
}
/// <summary>
/// Attempts to acquire an exclusive lock for the specified <paramref name="key"/>, waiting up to <paramref name="timeout"/>.
/// </summary>
/// <param name="key">A non-null object that identifies the lock.</param>
/// <param name="timeout">Maximum time to wait for the lock. If set to <see cref="TimeSpan.Zero"/>, the method performs an immediate, non-blocking attempt.</param>
/// <param name="cancellationToken">A token to cancel the wait.</param>
/// <returns>
/// An <see cref="IDisposable"/> handle if the lock was acquired within the timeout; otherwise <see langword="null"/>.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
/// <exception cref="OperationCanceledException">Thrown if the wait is canceled via <paramref name="cancellationToken"/>.</exception>
/// <example>
/// <code>
/// var handle = await KeyedLock.TryLockAsync("send-mail", TimeSpan.FromSeconds(1));
/// if (handle != null)
/// {
/// using (handle)
/// {
/// // protected work
/// }
/// }
/// else
/// {
/// // lock not acquired within timeout
/// }
/// </code>
/// </example>
public static async Task < IDisposable ? > TryLockAsync ( object key , TimeSpan timeout , CancellationToken cancellationToken = default )
public static async Task < IDisposable ? > TryLockAsync ( object key , TimeSpan timeout , CancellationToken cancellationToken = default )
{
{
Check . NotNull ( key , nameof ( key ) ) ;
var semaphore = GetOrCreate ( key ) ;
var semaphore = GetOrCreate ( key ) ;
bool acquired ;
bool acquired ;
try
try