Browse Source

Enhance KeyedLock with additional locking methods and improve documentation

pull/24502/head
maliming 9 months ago
parent
commit
068a568bcc
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 91
      framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs

91
framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs

@ -6,20 +6,58 @@ using System.Threading.Tasks;
namespace Volo.Abp.Threading;
/// <summary>
/// Per-key asynchronous lock.
/// https://stackoverflow.com/a/31194647
/// Per-key asynchronous lock for coordinating concurrent flows.
/// </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
{
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)
{
Check.NotNull(key, nameof(key));
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)
{
Check.NotNull(key, nameof(key));
var semaphore = GetOrCreate(key);
try
{
@ -34,13 +72,62 @@ public static class KeyedLock
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)
{
Check.NotNull(key, nameof(key));
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)
{
Check.NotNull(key, nameof(key));
var semaphore = GetOrCreate(key);
bool acquired;
try

Loading…
Cancel
Save